settings: add django-rule and add contrib app implementing common model and admin implementing object level permissions using django-rule

object-level-permissions
Christian Merten 3 years ago
parent bb0d3f1d07
commit 1b06aff1a1
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -0,0 +1,201 @@
import copy
from django.contrib.auth import get_permission_codename
from django.core.exceptions import PermissionDenied
from django.contrib import admin, messages
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import path, reverse
from django.db import models
from django.contrib.admin import helpers, widgets
import rules.contrib.admin
from rules.permissions import perm_exists
class FieldPermissionsAdminMixin:
field_permissions = {}
def get_fields(self, request, obj=None):
fields = super(FieldPermissionsAdminMixin, self).get_fields(request, obj)
def may_field(field):
if field not in self.field_permissions:
return True
return request.user.has_perm(self.field_permissions[field], obj)
return list(filter(may_field, fields))
class ChangeViewAdminMixin:
def change_view(self, request, object_id, form_url="", extra_context=None):
try:
return super(ChangeViewAdminMixin, self).change_view(request, object_id,
form_url=form_url,
extra_context=extra_context)
except PermissionDenied:
opts = self.opts
obj = self.model.objects.get(pk=object_id)
messages.error(request,
_("You are not allowed to view %(name)s.") % {'name': str(obj)})
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name)))
class FilteredQuerysetAdminMixin:
def get_queryset(self, request):
"""
Return a QuerySet of all model instances that can be edited by the
admin site. This is used by changelist_view.
"""
qs = self.model._default_manager.get_queryset()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
queryset = qs
perm = '%s.list_global_%s' % (self.opts.app_label, self.opts.model_name)
if request.user.has_perm(perm):
return queryset
if not hasattr(request.user, 'member'):
return self.model.objects.none()
return request.user.member.filter_queryset_by_permissions(queryset, annotate=True, model=self.model)
#class ObjectPermissionsInlineModelAdminMixin(rules.contrib.admin.ObjectPermissionsInlineModelAdminMixin):
class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, FilteredQuerysetAdminMixin):
def has_add_permission(self, request, obj=None):
assert obj is None
opts = self.opts
codename = get_permission_codename("add_global", opts)
perm = "%s.%s" % (opts.app_label, codename)
return request.user.has_perm(perm, obj)
def has_view_permission(self, request, obj=None):
opts = self.opts
if obj is None:
codename = get_permission_codename("view", opts)
else:
codename = get_permission_codename("view_obj", opts)
perm = "%s.%s" % (opts.app_label, codename)
if perm_exists(perm):
return request.user.has_perm(perm, obj)
else:
return self.has_change_permission(request, obj)
def has_change_permission(self, request, obj=None):
opts = self.opts
if obj is None:
codename = get_permission_codename("view", opts)
else:
codename = get_permission_codename("change_obj", opts)
return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj)
def has_delete_permission(self, request, obj=None):
opts = self.opts
if obj is None:
codename = get_permission_codename("delete_global", opts)
else:
codename = get_permission_codename("delete_obj", opts)
return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj)
def formfield_for_dbfield(self, db_field, request, **kwargs):
"""
COPIED from django to disable related actions
Hook for specifying the form Field instance for a given database Field
instance.
If kwargs are given, they're passed to the form Field's constructor.
"""
# If the field specifies choices, we don't need to look for special
# admin widgets - we just need to use a select widget of some kind.
if db_field.choices:
return self.formfield_for_choice_field(db_field, request, **kwargs)
# ForeignKey or ManyToManyFields
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
# Combine the field kwargs with any options for formfield_overrides.
# Make sure the passed in **kwargs override anything in
# formfield_overrides because **kwargs is more specific, and should
# always win.
if db_field.__class__ in self.formfield_overrides:
kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs}
# Get the correct formfield.
if isinstance(db_field, models.ForeignKey):
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
elif isinstance(db_field, models.ManyToManyField):
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
formfield.widget = widgets.RelatedFieldWidgetWrapper(
formfield.widget,
db_field.remote_field,
self.admin_site,
)
return formfield
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
for klass in db_field.__class__.mro():
if klass in self.formfield_overrides:
kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs}
return db_field.formfield(**kwargs)
# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs)
class CommonAdminInlineMixin(CommonAdminMixin):
def has_add_permission(self, request, obj):
#assert obj is not None
if obj is None:
return True
if obj.pk is None:
return True
codename = get_permission_codename("add_obj", self.opts)
return request.user.has_perm('%s.%s' % (self.opts.app_label, codename), obj)
def has_view_permission(self, request, obj=None): # pragma: no cover
if obj is None:
return True
if obj.pk is None:
return True
opts = self.opts
if obj is None:
codename = get_permission_codename("view", opts)
else:
codename = get_permission_codename("view_obj", opts)
perm = "%s.%s" % (opts.app_label, codename)
if perm_exists(perm):
return request.user.has_perm(perm, obj)
else:
return self.has_change_permission(request, obj)
def has_change_permission(self, request, obj=None): # pragma: no cover
if obj is None:
return True
if obj.pk is None:
return True
opts = self.opts
if opts.auto_created:
for field in opts.fields:
if field.rel and field.rel.to != self.parent_model:
opts = field.rel.to._meta
break
codename = get_permission_codename("change_obj", opts)
return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj)
def has_delete_permission(self, request, obj=None): # pragma: no cover
if obj is None:
return True
if obj.pk is None:
return True
if self.opts.auto_created:
return self.has_change_permission(request, obj)
return super().has_delete_permission(request, obj)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ContribConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'contrib'

@ -0,0 +1,10 @@
from django.db import models
from rules.contrib.models import RulesModelBase, RulesModelMixin
# Create your models here.
class CommonModel(models.Model, RulesModelMixin, metaclass=RulesModelBase):
class Meta:
abstract = True
default_permissions = (
'add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view',
)

@ -0,0 +1,18 @@
from django.contrib.auth import get_permission_codename
import rules.contrib.admin
import rules
def memberize_user(func):
def inner(user, other):
if not hasattr(user, 'member'):
return False
return func(user.member, other)
return inner
def has_global_perm(name):
@rules.predicate
def pred(user, obj):
return user.has_perm(name)
return pred

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -38,6 +38,7 @@ USE_X_FORWARDED_HOST = True
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'contrib.apps.ContribConfig',
'startpage.apps.StartpageConfig', 'startpage.apps.StartpageConfig',
'material.apps.MaterialConfig', 'material.apps.MaterialConfig',
'members.apps.MembersConfig', 'members.apps.MembersConfig',
@ -48,6 +49,7 @@ INSTALLED_APPS = [
'djcelery_email', 'djcelery_email',
'nested_admin', 'nested_admin',
'django_celery_beat', 'django_celery_beat',
'rules',
'jet', 'jet',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@ -91,6 +93,11 @@ TEMPLATES = [
WSGI_APPLICATION = 'jdav_web.wsgi.application' WSGI_APPLICATION = 'jdav_web.wsgi.application'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'rules.permissions.ObjectPermissionBackend',
)
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

@ -33,6 +33,7 @@ python-monkey-business==1.0.0
pytz==2021.3 pytz==2021.3
redis==4.1.0 redis==4.1.0
requests==2.27.1 requests==2.27.1
rules==3.3
six==1.16.0 six==1.16.0
sqlparse==0.4.2 sqlparse==0.4.2
tzdata==2022.7 tzdata==2022.7

Loading…
Cancel
Save