diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index d20987f8e1..f4b1722757 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -15,11 +15,17 @@ class BaseBackend: def get_user(self, user_id): return None + def get_user_permissions(self, user_obj, obj=None): + return set() + def get_group_permissions(self, user_obj, obj=None): return set() def get_all_permissions(self, user_obj, obj=None): - return self.get_group_permissions(user_obj, obj=obj) + return { + *self.get_user_permissions(user_obj, obj=obj), + *self.get_group_permissions(user_obj, obj=obj), + } def has_perm(self, user_obj, perm, obj=None): return perm in self.get_all_permissions(user_obj, obj=obj) @@ -96,10 +102,7 @@ class ModelBackend(BaseBackend): if not user_obj.is_active or user_obj.is_anonymous or obj is not None: return set() if not hasattr(user_obj, '_perm_cache'): - user_obj._perm_cache = { - *self.get_user_permissions(user_obj), - *self.get_group_permissions(user_obj), - } + user_obj._perm_cache = super().get_all_permissions(user_obj) return user_obj._perm_cache def has_perm(self, user_obj, perm, obj=None): diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 324faa6f3e..78fd7fbb5c 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -159,11 +159,12 @@ class UserManager(BaseUserManager): # A few helper functions for common logic between User and AnonymousUser. -def _user_get_all_permissions(user, obj): +def _user_get_permissions(user, obj, from_name): permissions = set() + name = 'get_%s_permissions' % from_name for backend in auth.get_backends(): - if hasattr(backend, "get_all_permissions"): - permissions.update(backend.get_all_permissions(user, obj)) + if hasattr(backend, name): + permissions.update(getattr(backend, name)(user, obj)) return permissions @@ -233,20 +234,24 @@ class PermissionsMixin(models.Model): class Meta: abstract = True + def get_user_permissions(self, obj=None): + """ + Return a list of permission strings that this user has directly. + Query all available auth backends. If an object is passed in, + return only permissions matching this object. + """ + return _user_get_permissions(self, obj, 'user') + def get_group_permissions(self, obj=None): """ Return a list of permission strings that this user has through their groups. Query all available auth backends. If an object is passed in, return only permissions matching this object. """ - permissions = set() - for backend in auth.get_backends(): - if hasattr(backend, "get_group_permissions"): - permissions.update(backend.get_group_permissions(self, obj)) - return permissions + return _user_get_permissions(self, obj, 'group') def get_all_permissions(self, obj=None): - return _user_get_all_permissions(self, obj) + return _user_get_permissions(self, obj, 'all') def has_perm(self, perm, obj=None): """ @@ -403,11 +408,14 @@ class AnonymousUser: def user_permissions(self): return self._user_permissions + def get_user_permissions(self, obj=None): + return _user_get_permissions(self, obj, 'user') + def get_group_permissions(self, obj=None): return set() def get_all_permissions(self, obj=None): - return _user_get_all_permissions(self, obj=obj) + return _user_get_permissions(self, obj, 'all') def has_perm(self, perm, obj=None): return _user_has_perm(self, perm, obj=obj) diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 17197e9fc7..877ec168e5 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -191,6 +191,15 @@ Methods :meth:`~django.contrib.auth.models.User.set_unusable_password()` has been called for this user. + .. method:: get_user_permissions(obj=None) + + .. versionadded:: 3.0 + + Returns a set of permission strings that the user has directly. + + If ``obj`` is passed in, only returns the user permissions for this + specific object. + .. method:: get_group_permissions(obj=None) Returns a set of permission strings that the user has, through their @@ -467,14 +476,18 @@ The following backends are available in :mod:`django.contrib.auth.backends`: A base class that provides default implementations for all required methods. By default, it will reject any user and provide no permissions. + .. method:: get_user_permissions(user_obj, obj=None) + + Returns an empty set. + .. method:: get_group_permissions(user_obj, obj=None) Returns an empty set. .. method:: get_all_permissions(user_obj, obj=None) - Uses :meth:`get_group_permissions` to get the set of permission strings - the ``user_obj`` has. + Uses :meth:`get_user_permissions` and :meth:`get_group_permissions` to + get the set of permission strings the ``user_obj`` has. .. method:: has_perm(user_obj, perm, obj=None) diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 43eb07c78c..1ba81c5643 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -72,6 +72,10 @@ Minor features * Added :class:`~django.contrib.auth.backends.BaseBackend` class to ease customization of authentication backends. +* Added :meth:`~django.contrib.auth.models.User.get_user_permissions()` method + to mirror the existing + :meth:`~django.contrib.auth.models.User.get_group_permissions()` method. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index becf30e7e6..282eb8ca91 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -180,7 +180,8 @@ Handling authorization in custom backends Custom auth backends can provide their own permissions. The user model will delegate permission lookup functions -(:meth:`~django.contrib.auth.models.User.get_group_permissions()`, +(:meth:`~django.contrib.auth.models.User.get_user_permissions()`, +:meth:`~django.contrib.auth.models.User.get_group_permissions()`, :meth:`~django.contrib.auth.models.User.get_all_permissions()`, :meth:`~django.contrib.auth.models.User.has_perm()`, and :meth:`~django.contrib.auth.models.User.has_module_perms()`) to any @@ -898,6 +899,15 @@ methods and attributes: Boolean. Designates that this user has all permissions without explicitly assigning them. + .. method:: models.PermissionsMixin.get_user_permissions(obj=None) + + .. versionadded:: 3.0 + + Returns a set of permission strings that the user has directly. + + If ``obj`` is passed in, only returns the user permissions for this + specific object. + .. method:: models.PermissionsMixin.get_group_permissions(obj=None) Returns a set of permission strings that the user has, through their diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py index 84facd6c3a..6447edefc9 100644 --- a/tests/auth_tests/test_auth_backends.py +++ b/tests/auth_tests/test_auth_backends.py @@ -21,6 +21,9 @@ from .models import ( class SimpleBackend(BaseBackend): + def get_user_permissions(self, user_obj, obj=None): + return ['user_perm'] + def get_group_permissions(self, user_obj, obj=None): return ['group_perm'] @@ -31,13 +34,17 @@ class BaseBackendTest(TestCase): def setUpTestData(cls): cls.user = User.objects.create_user('test', 'test@example.com', 'test') + def test_get_user_permissions(self): + self.assertEqual(self.user.get_user_permissions(), {'user_perm'}) + def test_get_group_permissions(self): self.assertEqual(self.user.get_group_permissions(), {'group_perm'}) def test_get_all_permissions(self): - self.assertEqual(self.user.get_all_permissions(), {'group_perm'}) + self.assertEqual(self.user.get_all_permissions(), {'user_perm', 'group_perm'}) def test_has_perm(self): + self.assertIs(self.user.has_perm('user_perm'), True) self.assertIs(self.user.has_perm('group_perm'), True) self.assertIs(self.user.has_perm('other_perm', TestObj()), False) @@ -102,6 +109,7 @@ class BaseModelBackendTest: # reloading user to purge the _perm_cache user = self.UserModel._default_manager.get(pk=self.user.pk) self.assertEqual(user.get_all_permissions(), {'auth.test'}) + self.assertEqual(user.get_user_permissions(), {'auth.test'}) self.assertEqual(user.get_group_permissions(), set()) self.assertIs(user.has_module_perms('Group'), False) self.assertIs(user.has_module_perms('auth'), True) @@ -111,7 +119,8 @@ class BaseModelBackendTest: perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3') user.user_permissions.add(perm) user = self.UserModel._default_manager.get(pk=self.user.pk) - self.assertEqual(user.get_all_permissions(), {'auth.test2', 'auth.test', 'auth.test3'}) + expected_user_perms = {'auth.test2', 'auth.test', 'auth.test3'} + self.assertEqual(user.get_all_permissions(), expected_user_perms) self.assertIs(user.has_perm('test'), False) self.assertIs(user.has_perm('auth.test'), True) self.assertIs(user.has_perms(['auth.test2', 'auth.test3']), True) @@ -121,8 +130,8 @@ class BaseModelBackendTest: group.permissions.add(perm) user.groups.add(group) user = self.UserModel._default_manager.get(pk=self.user.pk) - exp = {'auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'} - self.assertEqual(user.get_all_permissions(), exp) + self.assertEqual(user.get_all_permissions(), {*expected_user_perms, 'auth.test_group'}) + self.assertEqual(user.get_user_permissions(), expected_user_perms) self.assertEqual(user.get_group_permissions(), {'auth.test_group'}) self.assertIs(user.has_perms(['auth.test3', 'auth.test_group']), True) diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py index dd3377d7a6..a3502a224f 100644 --- a/tests/auth_tests/test_models.py +++ b/tests/auth_tests/test_models.py @@ -333,6 +333,7 @@ class AnonymousUserTests(SimpleTestCase): self.assertIs(self.user.is_superuser, False) self.assertEqual(self.user.groups.all().count(), 0) self.assertEqual(self.user.user_permissions.all().count(), 0) + self.assertEqual(self.user.get_user_permissions(), set()) self.assertEqual(self.user.get_group_permissions(), set()) def test_str(self):