Skip to content

Commit

Permalink
Feature: Custom generic object permission models (#657)
Browse files Browse the repository at this point in the history
* GUARDIAN_USER_OBJ_PERMS_USE and GUARDIAN_GROUP_OBJ_PERMS_USE support

* Document GUARDIAN_USER_OBJ_PERMS_USE and GUARDIAN_GROUP_OBJ_PERMS_USE

* example_project using GUARDIAN_USER_OBJ_PERMS_USE and GUARDIAN_GROUP_OBJ_PERMS_USE

* Update docs/configuration.rst

Co-Authored-By: Michael <michael-k@users.noreply.github.com>

* Conf settings should end in _MODEL, not _USE

* Explicit exception chaining during model lookup

* Fix typo in exception chaining

* Add __all__ = […] to silence unused-import warnings in models/__init__

* Code comment should use _MODEL, not _USE
  • Loading branch information
partimer authored and brianmay committed Nov 27, 2019
1 parent 16e8713 commit d00b80f
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 20 deletions.
62 changes: 62 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,65 @@ not be compatible with non-standard deployments, and should only be used when no
invocations would result in a large number of queries or when latency is particularly important.

Defaults to ``False``.

GUARDIAN_USER_OBJ_PERMS_MODEL
-------------------------

.. versionadded:: 2.x.x

Allows the default ``UserObjectPermission`` model to be overridden by a custom model. The custom model needs to minimally inherit from ``UserObjectPermissionAbstract``. This is only automatically supported when set at the start of a project. This is NOT supported after the start of a project. If the dependent libraries do not call ``UserObjectPermission = get_user_obj_perms_model()`` for the model, then the dependent library does not support this feature.

Define a custom user object permission model
::
from guardian.models import UserObjectPermissionAbstract
class BigUserObjectPermission(UserObjectPermissionAbstract):
id = models.BigAutoField(editable=False, unique=True, primary_key=True)
class Meta(UserObjectPermissionAbstract.Meta):
abstract = False
indexes = [
*UserObjectPermissionAbstract.Meta.indexes,
models.Index(fields=['content_type', 'object_pk', 'user']),
]


Configure guardian to use the custom model in ``settings.py``
::
GUARDIAN_USER_OBJ_PERMS_MODEL = 'myapp.BigUserObjectPermission'

To access the model use ``get_user_obj_perms_model()`` with no parameters
::
from guardian.utils import get_user_obj_perms_model
UserObjectPermission = get_user_obj_perms_model()

Defaults to ``'guardian.UserObjectPermission'``.

GUARDIAN_GROUP_OBJ_PERMS_MODEL
-------------------------

.. versionadded:: 2.x.x

Allows the default ``GroupObjectPermission`` model to be overridden by a custom model. The custom model needs to minimally inherit from ``GroupObjectPermissionAbstract``. This is only automatically supported when set at the start of a project. This is NOT supported after the start of a project. If the dependent libraries do not call ``GroupObjectPermission = get_user_obj_perms_model()`` for the model, then the dependent library does not support this feature.

Define a custom user object permission model
::
from guardian.models import GroupObjectPermissionAbstract
class BigGroupObjectPermission(GroupObjectPermissionAbstract):
id = models.BigAutoField(editable=False, unique=True, primary_key=True)
class Meta(GroupObjectPermissionAbstract.Meta):
abstract = False
indexes = [
*GroupObjectPermissionAbstract.Meta.indexes,
models.Index(fields=['content_type', 'object_pk', 'group']),
]


Configure guardian to use the custom model in `settings.py`
::
GUARDIAN_GROUP_OBJ_PERMS_MODEL = 'myapp.BigGroupObjectPermission'

To access the model use ``get_user_obj_perms_model()`` with no parameters
::
from guardian.utils import get_user_obj_perms_model
GroupObjectPermission = get_user_obj_perms_model()

Defaults to ``'guardian.GroupObjectPermission'``.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generated by Django 2.1.14 on 2019-11-14 10:41

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('articles', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='BigGroupObjectPermission',
fields=[
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False, unique=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Permission')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='BigUserObjectPermission',
fields=[
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False, unique=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Permission')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddIndex(
model_name='biguserobjectpermission',
index=models.Index(fields=['content_type', 'object_pk'], name='articles_bi_content_3fff51_idx'),
),
migrations.AddIndex(
model_name='biguserobjectpermission',
index=models.Index(fields=['content_type', 'object_pk', 'user'], name='articles_bi_content_a2ac4b_idx'),
),
migrations.AlterUniqueTogether(
name='biguserobjectpermission',
unique_together={('user', 'permission', 'object_pk')},
),
migrations.AddIndex(
model_name='biggroupobjectpermission',
index=models.Index(fields=['content_type', 'object_pk'], name='articles_bi_content_824ecd_idx'),
),
migrations.AddIndex(
model_name='biggroupobjectpermission',
index=models.Index(fields=['content_type', 'object_pk', 'group'], name='articles_bi_content_61c3ef_idx'),
),
migrations.AlterUniqueTogether(
name='biggroupobjectpermission',
unique_together={('group', 'permission', 'object_pk')},
),
]
23 changes: 23 additions & 0 deletions example_project/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,26 @@ class ArticleUserObjectPermission(UserObjectPermissionBase):

class ArticleGroupObjectPermission(GroupObjectPermissionBase):
content_object = models.ForeignKey(Article, on_delete=models.CASCADE)


from guardian.models import UserObjectPermissionAbstract, GroupObjectPermissionAbstract

class BigUserObjectPermission(UserObjectPermissionAbstract):
id = models.BigAutoField(editable=False, unique=True, primary_key=True)
class Meta(UserObjectPermissionAbstract.Meta):
abstract = False
indexes = [
*UserObjectPermissionAbstract.Meta.indexes,
models.Index(fields=['content_type', 'object_pk', 'user']),
]


class BigGroupObjectPermission(GroupObjectPermissionAbstract):
id = models.BigAutoField(editable=False, unique=True, primary_key=True)
class Meta(GroupObjectPermissionAbstract.Meta):
abstract = False
indexes = [
*GroupObjectPermissionAbstract.Meta.indexes,
models.Index(fields=['content_type', 'object_pk', 'group']),
]

3 changes: 2 additions & 1 deletion example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
)

AUTH_USER_MODEL = 'core.CustomUser'

GUARDIAN_USER_OBJ_PERMS_MODEL = 'articles.BigUserObjectPermission'
GUARDIAN_GROUP_OBJ_PERMS_MODEL = 'articles.BigGroupObjectPermission'

TEMPLATES = [
{
Expand Down
14 changes: 13 additions & 1 deletion guardian/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def get_version():
def monkey_patch_user():
from django.contrib.auth import get_user_model
from .utils import get_anonymous_user, evict_obj_perms_cache
from .models import UserObjectPermission
from .utils import get_user_obj_perms_model
UserObjectPermission = get_user_obj_perms_model()
User = get_user_model()
# Prototype User and Group methods
setattr(User, 'get_anonymous', staticmethod(lambda: get_anonymous_user()))
Expand All @@ -28,3 +29,14 @@ def monkey_patch_user():
setattr(User, 'del_obj_perm',
lambda self, perm, obj: UserObjectPermission.objects.remove_perm(perm, self, obj))
setattr(User, 'evict_obj_perms_cache', evict_obj_perms_cache)


def monkey_patch_group():
from django.contrib.auth.models import Group, Permission
from .utils import get_group_obj_perms_model
GroupObjectPermission = get_group_obj_perms_model()
# Prototype Group methods
setattr(Group, 'add_obj_perm',
lambda self, perm, obj: GroupObjectPermission.objects.assign_perm(perm, self, obj))
setattr(Group, 'del_obj_perm',
lambda self, perm, obj: GroupObjectPermission.objects.remove_perm(perm, self, obj))
2 changes: 1 addition & 1 deletion guardian/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from guardian.forms import GroupObjectPermissionsForm, UserObjectPermissionsForm
from guardian.models import Group
from django.contrib.auth.models import Group
from guardian.shortcuts import (get_group_perms, get_groups_with_perms, get_perms_for_model, get_user_perms,
get_users_with_perms)

Expand Down
5 changes: 4 additions & 1 deletion guardian/apps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import monkey_patch_user
from . import monkey_patch_user, monkey_patch_group
from django.apps import AppConfig
from guardian.conf import settings

Expand All @@ -7,5 +7,8 @@ class GuardianConfig(AppConfig):
name = 'guardian'

def ready(self):
# Must patch Group here since generic
# group permission model is definable
monkey_patch_group()
if settings.MONKEY_PATCH:
monkey_patch_user()
4 changes: 4 additions & 0 deletions guardian/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@

AUTO_PREFETCH = getattr(settings, 'GUARDIAN_AUTO_PREFETCH', False)

# Default to using guardian supplied generic object permission models
USER_OBJ_PERMS_MODEL = getattr(settings, 'GUARDIAN_USER_OBJ_PERMS_MODEL', 'guardian.UserObjectPermission')
GROUP_OBJ_PERMS_MODEL = getattr(settings, 'GUARDIAN_GROUP_OBJ_PERMS_MODEL', 'guardian.GroupObjectPermission')


def check_configuration():
if RENDER_403 and RAISE_403:
Expand Down
4 changes: 3 additions & 1 deletion guardian/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@ def _init_obj_prefetch_cache(obj, *querysets):
return obj, cache

def _prefetch_cache(self):
from guardian.models import UserObjectPermission, GroupObjectPermission
from guardian.utils import get_user_obj_perms_model, get_group_obj_perms_model
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
if self.user:
obj = self.user
querysets = [
Expand Down
2 changes: 1 addition & 1 deletion guardian/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from guardian.core import ObjectPermissionChecker
from guardian.ctypes import get_content_type
from guardian.exceptions import ObjectNotPersisted
from guardian.models import Permission
from django.contrib.auth.models import Permission

import warnings

Expand Down
3 changes: 2 additions & 1 deletion guardian/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from django.conf import settings
from django.contrib.auth.decorators import login_required, REDIRECT_FIELD_NAME
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from guardian.models import UserObjectPermission
from guardian.utils import get_user_obj_perms_model
UserObjectPermission = get_user_obj_perms_model()
from guardian.utils import get_40x_or_None, get_anonymous_user
from guardian.shortcuts import get_objects_for_user

Expand Down
27 changes: 27 additions & 0 deletions guardian/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from .models import (
UserObjectPermissionBase,
UserObjectPermissionAbstract,
GroupObjectPermissionBase,
GroupObjectPermissionAbstract,
Permission,
Group
)

# Must import after .models
# When .models is loaded, default generic object permissions are created
# The following statements may redirect external references to custom
# generic object permission models
from guardian.utils import get_user_obj_perms_model, get_group_obj_perms_model
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()

__all__ = [
'UserObjectPermissionBase',
'UserObjectPermissionAbstract',
'GroupObjectPermissionBase',
'GroupObjectPermissionAbstract',
'Permission',
'Group',
'UserObjectPermission',
'GroupObjectPermission'
]
19 changes: 13 additions & 6 deletions guardian/models.py → guardian/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,19 @@ class Meta:
unique_together = ['user', 'permission', 'content_object']


class UserObjectPermission(UserObjectPermissionBase, BaseGenericObjectPermission):
class UserObjectPermissionAbstract(UserObjectPermissionBase, BaseGenericObjectPermission):

class Meta(UserObjectPermissionBase.Meta, BaseGenericObjectPermission.Meta):
abstract = True
unique_together = ['user', 'permission', 'object_pk']


class UserObjectPermission(UserObjectPermissionAbstract):

class Meta(UserObjectPermissionAbstract.Meta):
abstract = False


class GroupObjectPermissionBase(BaseObjectPermission):
"""
**Manager**: :manager:`GroupObjectPermissionManager`
Expand All @@ -78,13 +85,13 @@ class Meta:
unique_together = ['group', 'permission', 'content_object']


class GroupObjectPermission(GroupObjectPermissionBase, BaseGenericObjectPermission):
class GroupObjectPermissionAbstract(GroupObjectPermissionBase, BaseGenericObjectPermission):

class Meta(GroupObjectPermissionBase.Meta, BaseGenericObjectPermission.Meta):
abstract = True
unique_together = ['group', 'permission', 'object_pk']

class GroupObjectPermission(GroupObjectPermissionAbstract):

setattr(Group, 'add_obj_perm',
lambda self, perm, obj: GroupObjectPermission.objects.assign_perm(perm, self, obj))
setattr(Group, 'del_obj_perm',
lambda self, perm, obj: GroupObjectPermission.objects.remove_perm(perm, self, obj))
class Meta(GroupObjectPermissionAbstract.Meta):
abstract = False
3 changes: 2 additions & 1 deletion guardian/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
from guardian.core import ObjectPermissionChecker
from guardian.ctypes import get_content_type
from guardian.exceptions import MixedContentTypeError, WrongAppError, MultipleIdentityAndObjectError
from guardian.models import GroupObjectPermission
from guardian.utils import get_anonymous_user, get_group_obj_perms_model, get_identity, get_user_obj_perms_model
GroupObjectPermission = get_group_obj_perms_model()
UserObjectPermission = get_user_obj_perms_model()


def assign_perm(perm, user_or_group, obj=None):
Expand Down

0 comments on commit d00b80f

Please sign in to comment.