Add ArrayField on ObjectPermission to store actions

This commit is contained in:
Jeremy Stretch 2020-05-29 11:18:22 -04:00
parent 90828cedae
commit 02687453f2
7 changed files with 120 additions and 78 deletions

View File

@ -203,7 +203,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -227,7 +227,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -261,8 +261,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True,
can_add=True
actions=['view', 'add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -309,8 +308,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True,
can_change=True
actions=['view', 'change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -353,8 +351,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True,
can_delete=True
actions=['view', 'delete']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -402,7 +399,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -451,7 +448,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -495,8 +492,7 @@ class ObjectPermissionViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True,
can_delete=True
actions=['view', 'delete']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -567,7 +563,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -594,7 +590,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -621,7 +617,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -650,7 +646,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -685,7 +681,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
# Assign object permission
obj_perm = ObjectPermission(
attrs={'site__name': 'Site 1'},
can_delete=True
actions=['delete']
)
obj_perm.save()
obj_perm.users.add(self.user)

View File

@ -2,9 +2,10 @@ from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdmin_
from django.contrib.auth.models import Group as StockGroup, User as StockUser
from django.core.exceptions import FieldError, ValidationError
from extras.admin import order_content_types
from .models import Group, User, ObjectPermission, Token, UserConfig
from .models import AdminGroup, AdminUser, ObjectPermission, Token, UserConfig
#
@ -16,7 +17,7 @@ admin.site.unregister(StockGroup)
admin.site.unregister(StockUser)
@admin.register(Group)
@admin.register(AdminGroup)
class GroupAdmin(admin.ModelAdmin):
fields = ('name',)
list_display = ('name', 'user_count')
@ -34,7 +35,7 @@ class UserConfigInline(admin.TabularInline):
verbose_name = 'Preferences'
@admin.register(User)
@admin.register(AdminUser)
class UserAdmin(UserAdmin_):
list_display = [
'username', 'email', 'first_name', 'last_name', 'is_superuser', 'is_staff', 'is_active'
@ -92,10 +93,41 @@ class ObjectPermissionForm(forms.ModelForm):
order_content_types(self.fields['content_types'])
self.fields['content_types'].choices.insert(0, ('', '---------'))
def clean(self):
content_types = self.cleaned_data['content_types']
attrs = self.cleaned_data['attrs']
# Validate the specified model attributes by attempting to execute a query. We don't care whether the query
# returns anything; we just want to make sure the specified attributes are valid.
if attrs:
for ct in content_types:
model = ct.model_class()
try:
model.objects.filter(**attrs).exists()
except FieldError as e:
raise ValidationError({
'attrs': f'Invalid attributes for {model}: {e}'
})
@admin.register(ObjectPermission)
class ObjectPermissionAdmin(admin.ModelAdmin):
form = ObjectPermissionForm
list_display = [
'model', 'can_view', 'can_add', 'can_change', 'can_delete'
'list_models', 'list_users', 'list_groups', 'actions', 'attrs',
]
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('content_types', 'users', 'groups')
def list_models(self, obj):
return ', '.join([f"{ct}" for ct in obj.content_types.all()])
list_models.short_description = 'Models'
def list_users(self, obj):
return ', '.join([u.username for u in obj.users.all()])
list_users.short_description = 'Users'
def list_groups(self, obj):
return ', '.join([g.name for g in obj.groups.all()])
list_groups.short_description = 'Groups'

View File

@ -14,7 +14,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Group',
name='AdminGroup',
fields=[
],
options={
@ -28,7 +28,7 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
name='User',
name='AdminUser',
fields=[
],
options={

View File

@ -0,0 +1,33 @@
# Generated by Django 3.0.6 on 2020-05-29 14:59
from django.conf import settings
import django.contrib.postgres.fields
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('auth', '0011_update_proxy_permissions'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0007_proxy_group_user'),
]
operations = [
migrations.CreateModel(
name='ObjectPermission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('attrs', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
('actions', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), size=None)),
('content_types', models.ManyToManyField(limit_choices_to={'app_label__in': ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'virtualization']}, related_name='object_permissions', to='contenttypes.ContentType')),
('groups', models.ManyToManyField(blank=True, related_name='object_permissions', to='auth.Group')),
('users', models.ManyToManyField(blank=True, related_name='object_permissions', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Permission',
},
),
]

View File

@ -1,10 +1,9 @@
import binascii
import os
from django.contrib.auth.models import Group as Group_, User as User_
from django.contrib.auth.models import Group, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import FieldError, ValidationError
from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.validators import MinLengthValidator
from django.db import models
from django.db.models.signals import post_save
@ -25,7 +24,7 @@ __all__ = (
# Proxy models for admin
#
class Group(Group_):
class AdminGroup(Group):
"""
Proxy contrib.auth.models.Group for the admin UI
"""
@ -33,7 +32,7 @@ class Group(Group_):
proxy = True
class User(User_):
class AdminUser(User):
"""
Proxy contrib.auth.models.User for the admin UI
"""
@ -256,31 +255,13 @@ class ObjectPermission(models.Model):
null=True,
verbose_name='Attributes'
)
can_view = models.BooleanField(
default=False
)
can_add = models.BooleanField(
default=False
)
can_change = models.BooleanField(
default=False
)
can_delete = models.BooleanField(
default=False
actions = ArrayField(
base_field=models.CharField(max_length=30),
help_text="The list of actions granted by this permission"
)
class Meta:
verbose_name = "Permission"
def __str__(self):
return "Object permission"
def clean(self):
# Validate the specified model attributes by attempting to execute a query. We don't care whether the query
# returns anything; we just want to make sure the specified attributes are valid.
if self.attrs:
model = self.model.model_class()
try:
model.objects.filter(**self.attrs).exists()
except FieldError as e:
raise ValidationError({
'attrs': f'Invalid attributes for {model}: {e}'
})

View File

@ -32,13 +32,12 @@ class ObjectPermissionBackend(ModelBackend):
perms = dict()
for obj_perm in object_permissions:
for content_type in obj_perm.content_types.all():
for action in ['view', 'add', 'change', 'delete']:
if getattr(obj_perm, f"can_{action}"):
perm_name = f"{content_type.app_label}.{action}_{content_type.model}"
if perm_name in perms:
perms[perm_name].append(obj_perm.attrs)
else:
perms[perm_name] = [obj_perm.attrs]
for action in obj_perm.actions:
perm_name = f"{content_type.app_label}.{action}_{content_type.model}"
if perm_name in perms:
perms[perm_name].append(obj_perm.attrs)
else:
perms[perm_name] = [obj_perm.attrs]
return perms
@ -123,7 +122,8 @@ class RemoteUserBackend(_RemoteUserBackend):
for permission_name in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS:
try:
content_type, action = resolve_permission(permission_name)
obj_perm = ObjectPermission(**{f'can_{action}': True})
# TODO: Merge multiple actions into a single ObjectPermission per content type
obj_perm = ObjectPermission(actions=[action])
obj_perm.save()
obj_perm.users.add(user)
obj_perm.content_types.add(content_type)

View File

@ -34,7 +34,7 @@ class TestCase(_TestCase):
"""
for name in names:
ct, action = resolve_permission(name)
obj_perm = ObjectPermission(**{f'can_{action}': True})
obj_perm = ObjectPermission(actions=[action])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.content_types.add(ct)
@ -165,7 +165,7 @@ class ViewTestCases:
# Add model-level permission
obj_perm = ObjectPermission(
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -181,7 +181,7 @@ class ViewTestCases:
# Add object-level permission
obj_perm = ObjectPermission(
attrs={'pk': instance1.pk},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -221,7 +221,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -246,7 +246,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk__gt': 0}, # Dummy permission to allow all
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -305,7 +305,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -329,7 +329,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk': instance1.pk},
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -382,7 +382,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_delete=True
actions=['delete']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -407,7 +407,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk': instance1.pk},
can_delete=True
actions=['delete']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -459,7 +459,7 @@ class ViewTestCases:
# Add model-level permission
obj_perm = ObjectPermission(
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -481,7 +481,7 @@ class ViewTestCases:
# Add object-level permission
obj_perm = ObjectPermission(
attrs={'pk': instance1.pk},
can_view=True
actions=['view']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -512,7 +512,7 @@ class ViewTestCases:
self.assertHttpStatus(self.client.post(**request), 403)
# Assign object-level permission
obj_perm = ObjectPermission(can_add=True)
obj_perm = ObjectPermission(actions=['add'])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.content_types.add(ContentType.objects.get_for_model(self.model))
@ -557,7 +557,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -580,7 +580,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk__gt': 0}, # Dummy permission to allow all
can_add=True
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -627,7 +627,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -652,7 +652,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk__in': list(pk_list)},
can_change=True
actions=['change']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -697,7 +697,7 @@ class ViewTestCases:
# Assign model-level permission
obj_perm = ObjectPermission(
can_delete=True
actions=['delete']
)
obj_perm.save()
obj_perm.users.add(self.user)
@ -719,7 +719,7 @@ class ViewTestCases:
# Assign object-level permission
obj_perm = ObjectPermission(
attrs={'pk__in': list(pk_list)},
can_delete=True
actions=['delete']
)
obj_perm.save()
obj_perm.users.add(self.user)