24 декабря 2018 г.

django: фильтр нужных разрешений юзера в админке

В админке Django всегда делаю чтобы в настройках профиля и настройках группы светились только нужные, используемые в логике пермишены (например, свои или какие-то отдельные стандартные).
Иначе их обычно слишком много и непонятно какие в реальности имеют смысл.
Просто перегружаются Admin-классы и формы в них, где фильтруется ModelForm.queryset у нужного поля формы.

admin.py
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.forms import UserChangeForm
from django.db.models import Q
from django import forms

USED_PERMS = [
    'reports.add_report',
    'reports.change_report',
    'app2.custom_perm',
]

def qs_perm_filter(qs):
    ups = [up.split('.') for up in USED_PERMS]
    q_expressions = [Q(content_type__app_label=up[0], codename=up[1]) for up in ups]
    return qs.filter(reduce(operator.or_, q_expressions))
    
# наследуем форму (у меня там ещё много помимо этого), также см. нужную model
class UserProfileChangeForm(UserChangeForm):
    class Meta:
        model = UserProfile
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(UserProfileChangeForm, self).__init__(*args, **kwargs)

        f = self.fields.get('user_permissions')
        if f is not None:
            f.queryset = qs_perm_filter(f.queryset)

# наследуем кастомный UserAdmin
class UserProfileAdmin(UserAdmin):
    ...
    form = UserProfileChangeForm
    ...
    
# показываем профиль, родной User не показываем
admin.site.unregister(User)
admin.site.register(UserProfile, UserProfileAdmin)

# для групп тоже аналогично всё
class MyGroupAdminForm(forms.ModelForm):
    class Meta:
        model = Group
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(MyGroupAdminForm, self).__init__(*args, **kwargs)

        f = self.fields.get('permissions')
        if f is not None:
            f.queryset = qs_perm_filter(f.queryset)


class MyGroupAdmin(GroupAdmin):
    form = MyGroupAdminForm

admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)

Также для бонуса - вынос пермишенов пользователя в таблицу с юзерами в админке. Из списка видно кто и каким правами обладает, там все группы, персональные права, указание на доступ к админке и т.д:

21 ноября 2018 г.

Django не давать логиниться одновременно с двух разных мест

Другими словами, не разрешаются две параллельные сессии. При новом входе старый разлогинивается.
Может понадобиться для какого-нибудь портала в интранете или чего-то такого секюрного.
Решается всё довольно банально, есть специальный сигнал user_logged_in.
Ищем сессию для этого юзера, если нашли - сбрасываем в базе, сбрасываем в текущем engine (если это применимо).
Если что, у меня настроен один из стандартных:
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
Код обработчика сигнала. Всё заполонил комментариями, думаю, там всё понятно.
from django.contrib.auth.signals import user_logged_in
from django.contrib import messages
from django.contrib.sessions.models import Session
from django.utils import timezone

def user_logged_in_handler(sender, request, user, **kwargs):
    # текущая сессия: request.session.session_key
    for session in Session.objects.filter(expire_date__gte=timezone.now()).exclude(session_key=request.session.session_key):
        if session.get_decoded().get('_auth_user_id', None) == str(user.id):
            # нашли какую-то сессию, делаем её expired
            session.expire_date = timezone.now()
            session.save()
            # далее нам надо очистить все данные сессии
            # сначала я пробовал так
            # SessionStore = session.get_session_store_class()
            # sesstor = SessionStore(session.session_key)
            # sesstor.flush()
            # где flush - это метод именно для моего бекенда cached_db
            # я ожидал, что для модели вернётся текущий настроенный в settings.SESSION_ENGINE бекенд, но в модели Session захардкожено (как ни странно?) db.SessionStore
            # в принципе можно напрямую тупо импортировать cached_db.SessionStore, но сделал проще и универсальнее:
            # request.session у нас уже содержит установленный мидлварей нужный SessionStore
            # с помощью "чужой" нашей новой SessionStore удаляем левый наш старый session_key, там удачно есть такой даже метод
            request.session.delete(session.session_key)
            # раз у нас доступен request, то при желании показываем юзеру сообщение
            messages.add_message(request, messages.INFO, 'Была закрыта дублирующаяся сессия на каком-то другом компьютере')

user_logged_in.connect(user_logged_in_handler)