From e95eb960a236aecf6f200f52c5253f63b97e668b Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 6 Oct 2022 16:57:40 +0200 Subject: [PATCH 2/2] custom_user: move permission mixin code from django_rbac (#69902) --- .../custom_user}/backends.py | 2 +- src/authentic2/custom_user/models.py | 136 +++++++++++++++- src/authentic2/manager/forms.py | 2 +- src/authentic2/models.py | 2 +- src/authentic2/settings.py | 2 +- src/django_rbac/models.py | 146 +----------------- tests/test_rbac.py | 3 +- 7 files changed, 140 insertions(+), 153 deletions(-) rename src/{django_rbac => authentic2/custom_user}/backends.py (99%) diff --git a/src/django_rbac/backends.py b/src/authentic2/custom_user/backends.py similarity index 99% rename from src/django_rbac/backends.py rename to src/authentic2/custom_user/backends.py index edd7389ec..631958fd0 100644 --- a/src/django_rbac/backends.py +++ b/src/authentic2/custom_user/backends.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.query import Q -from . import utils +from django_rbac import utils def get_fk_model(model, fieldname): diff --git a/src/authentic2/custom_user/models.py b/src/authentic2/custom_user/models.py index 968e0738d..bc8b7a50b 100644 --- a/src/authentic2/custom_user/models.py +++ b/src/authentic2/custom_user/models.py @@ -17,10 +17,15 @@ import base64 import datetime +import functools +import operator import os import uuid -from django.contrib.auth.models import AbstractBaseUser +from django.contrib import auth +from django.contrib.auth.models import AbstractBaseUser, Group +from django.contrib.auth.models import Permission as AuthPermission +from django.contrib.auth.models import _user_has_module_perms, _user_has_perm from django.contrib.contenttypes.fields import GenericRelation from django.contrib.postgres.fields import JSONField from django.core.exceptions import MultipleObjectsReturned, ValidationError @@ -30,6 +35,15 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ +try: + from django.contrib.auth.models import _user_get_all_permissions +except ImportError: + from django.contrib.auth.models import _user_get_permissions + + def _user_get_all_permissions(user, obj): + return _user_get_permissions(user, obj, 'all') + + from authentic2 import app_settings from authentic2.a2_rbac.models import RoleParenting from authentic2.decorators import errorcollector @@ -38,8 +52,8 @@ from authentic2.utils import misc as utils_misc from authentic2.utils.cache import RequestCache from authentic2.utils.models import generate_slug from authentic2.validators import email_validator -from django_rbac.models import PermissionMixin +from .backends import DjangoRBACBackend from .managers import UserManager, UserQuerySet @@ -132,7 +146,7 @@ class IsVerifiedDescriptor: return IsVerified(obj) -class User(AbstractBaseUser, PermissionMixin): +class User(AbstractBaseUser): """ An abstract base class implementing a fully featured User model with admin-compliant permissions. @@ -151,6 +165,11 @@ class User(AbstractBaseUser, PermissionMixin): email_verified_date = models.DateTimeField( default=None, blank=True, null=True, verbose_name=_('email verified date') ) + is_superuser = models.BooleanField( + _('superuser status'), + default=False, + help_text=_('Designates that this user has all permissions without explicitly assigning them.'), + ) is_staff = models.BooleanField( _('staff status'), default=False, @@ -172,6 +191,25 @@ class User(AbstractBaseUser, PermissionMixin): swappable=False, on_delete=models.CASCADE, ) + groups = models.ManyToManyField( + to=Group, + verbose_name=_('groups'), + blank=True, + help_text=_( + 'The groups this user belongs to. A user will get all permissions granted to each of his/her' + ' group.' + ), + related_name="user_set", + related_query_name="user", + ) + user_permissions = models.ManyToManyField( + to=AuthPermission, + verbose_name=_('user permissions'), + blank=True, + help_text=_('Specific permissions for this user.'), + related_name="user_set", + related_query_name="user", + ) # events dates date_joined = models.DateTimeField(_('date joined'), default=timezone.now) @@ -198,6 +236,98 @@ class User(AbstractBaseUser, PermissionMixin): verbose_name_plural = _('users') ordering = ('last_name', 'first_name', 'email', 'username') + def get_group_permissions(self, obj=None): + """ + Returns a list of permission strings that this user has through their + groups. This method queries all available auth backends. If an object + is passed in, only permissions matching this object are returned. + """ + permissions = set() + for backend in auth.get_backends(): + if hasattr(backend, "get_group_permissions"): + permissions.update(backend.get_group_permissions(self, obj)) + return permissions + + def get_all_permissions(self, obj=None): + return _user_get_all_permissions(self, obj) + + def has_perm(self, perm, obj=None): + """ + Returns True if the user has the specified permission. This method + queries all available auth backends, but returns immediately if any + backend returns True. Thus, a user who has permission from a single + auth backend is assumed to have permission in general. If an object is + provided, permissions for this specific object are checked. + """ + + # Active superusers have all permissions. + if self.is_active and self.is_superuser: + return True + + # Otherwise we need to check the backends. + return _user_has_perm(self, perm, obj) + + def has_perms(self, perm_list, obj=None): + """ + Returns True if the user has each of the specified permissions. If + object is passed, it checks if the user has all required perms for this + object. + """ + # Active superusers have all permissions. + if self.is_active and self.is_superuser: + return True + + for perm in perm_list: + if not self.has_perm(perm, obj): + return False + return True + + def has_module_perms(self, app_label): + """ + Returns True if the user has any permissions in the given app label. + Uses pretty much the same logic as has_perm, above. + """ + # Active superusers have all permissions. + if self.is_active and self.is_superuser: + return True + + return _user_has_module_perms(self, app_label) + + def filter_by_perm(self, perm_or_perms, qs): + results = [] + for backend in auth.get_backends(): + if hasattr(backend, "filter_by_perm"): + results.append(backend.filter_by_perm(self, perm_or_perms, qs)) + if results: + return functools.reduce(operator.__or__, results) + else: + return qs + + def has_perm_any(self, perm_or_perms): + # Active superusers have all permissions. + if self.is_active and self.is_superuser: + return True + + for backend in auth.get_backends(): + if hasattr(backend, "has_perm_any"): + if backend.has_perm_any(self, perm_or_perms): + return True + return False + + def has_ou_perm(self, perm, ou): + # Active superusers have all permissions. + if self.is_active and self.is_superuser: + return True + + for backend in auth.get_backends(): + if hasattr(backend, "has_ou_perm"): + if backend.has_ou_perm(self, perm, ou): + return True + return False + + def ous_with_perm(self, perm, queryset=None): + return DjangoRBACBackend().ous_with_perm(self, perm, queryset=queryset) + def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. diff --git a/src/authentic2/manager/forms.py b/src/authentic2/manager/forms.py index 8e8713d5d..b333b9a64 100644 --- a/src/authentic2/manager/forms.py +++ b/src/authentic2/manager/forms.py @@ -33,6 +33,7 @@ from django_select2.forms import HeavySelect2Widget from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role, RoleAttribute from authentic2.a2_rbac.utils import generate_slug, get_default_ou +from authentic2.custom_user.backends import DjangoRBACBackend from authentic2.forms.fields import ( CheckPasswordField, CommaSeparatedCharField, @@ -45,7 +46,6 @@ from authentic2.models import APIClient, PasswordReset, Service from authentic2.passwords import generate_password, get_min_password_strength from authentic2.utils.misc import send_email_change_email, send_password_reset_mail, send_templated_mail from authentic2.validators import EmailValidator -from django_rbac.backends import DjangoRBACBackend from django_rbac.models import Operation from . import app_settings, fields, utils diff --git a/src/authentic2/models.py b/src/authentic2/models.py index ce9ba9e01..9724fa38e 100644 --- a/src/authentic2/models.py +++ b/src/authentic2/models.py @@ -41,9 +41,9 @@ from model_utils.managers import QueryManager from authentic2.a2_rbac.models import Role from authentic2.a2_rbac.utils import get_default_ou_pk +from authentic2.custom_user.backends import DjangoRBACBackend from authentic2.utils.crypto import base64url_decode, base64url_encode from authentic2.validators import HexaColourValidator -from django_rbac.backends import DjangoRBACBackend # install our natural_key implementation from . import managers diff --git a/src/authentic2/settings.py b/src/authentic2/settings.py index db8f26b34..93a4760a5 100644 --- a/src/authentic2/settings.py +++ b/src/authentic2/settings.py @@ -165,7 +165,7 @@ AUTHENTICATION_BACKENDS = ( 'authentic2.backends.ldap_backend.LDAPBackend', 'authentic2.backends.ldap_backend.LDAPBackendPasswordLost', 'authentic2.backends.models_backend.DummyModelBackend', - 'django_rbac.backends.DjangoRBACBackend', + 'authentic2.custom_user.backends.DjangoRBACBackend', 'authentic2_auth_saml.backends.SAMLBackend', 'authentic2_auth_oidc.backends.OIDCBackend', 'authentic2_auth_fc.backends.FcBackend', diff --git a/src/django_rbac/models.py b/src/django_rbac/models.py index b181b8db2..858dd6635 100644 --- a/src/django_rbac/models.py +++ b/src/django_rbac/models.py @@ -1,25 +1,8 @@ -import functools -import operator - -from django.contrib import auth -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission as AuthPermission -from django.contrib.auth.models import _user_has_module_perms, _user_has_perm - -try: - from django.contrib.auth.models import _user_get_all_permissions -except ImportError: - from django.contrib.auth.models import _user_get_permissions - - def _user_get_all_permissions(user, obj): - return _user_get_permissions(user, obj, 'all') - - from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy -from . import backends, managers +from . import managers class Operation(models.Model): @@ -51,133 +34,6 @@ class Operation(models.Model): Operation._meta.natural_key = ['slug'] -class PermissionMixin(models.Model): - """ - A mixin class that adds the fields and methods necessary to support - Django's Group and Permission model using the ModelBackend. - """ - - is_superuser = models.BooleanField( - _('superuser status'), - default=False, - help_text=_('Designates that this user has all permissions without explicitly assigning them.'), - ) - groups = models.ManyToManyField( - to=Group, - verbose_name=_('groups'), - blank=True, - help_text=_( - 'The groups this user belongs to. A user will get all permissions granted to each of his/her' - ' group.' - ), - related_name="user_set", - related_query_name="user", - ) - user_permissions = models.ManyToManyField( - to=AuthPermission, - verbose_name=_('user permissions'), - blank=True, - help_text=_('Specific permissions for this user.'), - related_name="user_set", - related_query_name="user", - ) - - class Meta: - abstract = True - - def get_group_permissions(self, obj=None): - """ - Returns a list of permission strings that this user has through their - groups. This method queries all available auth backends. If an object - is passed in, only permissions matching this object are returned. - """ - permissions = set() - for backend in auth.get_backends(): - if hasattr(backend, "get_group_permissions"): - permissions.update(backend.get_group_permissions(self, obj)) - return permissions - - def get_all_permissions(self, obj=None): - return _user_get_all_permissions(self, obj) - - def has_perm(self, perm, obj=None): - """ - Returns True if the user has the specified permission. This method - queries all available auth backends, but returns immediately if any - backend returns True. Thus, a user who has permission from a single - auth backend is assumed to have permission in general. If an object is - provided, permissions for this specific object are checked. - """ - - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - # Otherwise we need to check the backends. - return _user_has_perm(self, perm, obj) - - def has_perms(self, perm_list, obj=None): - """ - Returns True if the user has each of the specified permissions. If - object is passed, it checks if the user has all required perms for this - object. - """ - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - for perm in perm_list: - if not self.has_perm(perm, obj): - return False - return True - - def has_module_perms(self, app_label): - """ - Returns True if the user has any permissions in the given app label. - Uses pretty much the same logic as has_perm, above. - """ - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - return _user_has_module_perms(self, app_label) - - def filter_by_perm(self, perm_or_perms, qs): - results = [] - for backend in auth.get_backends(): - if hasattr(backend, "filter_by_perm"): - results.append(backend.filter_by_perm(self, perm_or_perms, qs)) - if results: - return functools.reduce(operator.__or__, results) - else: - return qs - - def has_perm_any(self, perm_or_perms): - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - for backend in auth.get_backends(): - if hasattr(backend, "has_perm_any"): - if backend.has_perm_any(self, perm_or_perms): - return True - return False - - def has_ou_perm(self, perm, ou): - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - for backend in auth.get_backends(): - if hasattr(backend, "has_ou_perm"): - if backend.has_ou_perm(self, perm, ou): - return True - return False - - def ous_with_perm(self, perm, queryset=None): - return backends.DjangoRBACBackend().ous_with_perm(self, perm, queryset=queryset) - - ADMIN_OP = Operation.register(name=pgettext_lazy('permission', 'Management'), slug='admin') CHANGE_OP = Operation.register(name=pgettext_lazy('permission', 'Change'), slug='change') DELETE_OP = Operation.register(name=pgettext_lazy('permission', 'Delete'), slug='delete') diff --git a/tests/test_rbac.py b/tests/test_rbac.py index 6f8cef387..61aa69ee3 100644 --- a/tests/test_rbac.py +++ b/tests/test_rbac.py @@ -20,7 +20,8 @@ from django.db import connection from django.db.models import Q from django.test.utils import CaptureQueriesContext -from django_rbac import backends, models, utils +from authentic2.custom_user import backends +from django_rbac import models, utils OU = OrganizationalUnit = utils.get_ou_model() Permission = utils.get_permission_model() -- 2.35.1