Initial work on #554 (WIP)

This commit is contained in:
Jeremy Stretch
2020-05-08 17:30:25 -04:00
parent 43ad9aa2b1
commit 6624fc6076
6 changed files with 203 additions and 2 deletions

View File

@@ -3,7 +3,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdmin_
from django.contrib.auth.models import User
from .models import Token, UserConfig
from .models import ObjectPermission, Token, UserConfig
# Unregister the built-in UserAdmin so that we can use our custom admin view below
admin.site.unregister(User)
@@ -43,3 +43,10 @@ class TokenAdmin(admin.ModelAdmin):
list_display = [
'key', 'user', 'created', 'expires', 'write_enabled', 'description'
]
@admin.register(ObjectPermission)
class ObjectPermissionAdmin(admin.ModelAdmin):
list_display = [
'model', 'can_view', 'can_add', 'can_change', 'can_delete'
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 3.0.6 on 2020-05-08 20:18
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth', '0011_update_proxy_permissions'),
('contenttypes', '0002_remove_content_type_name'),
('users', '0006_create_userconfigs'),
]
operations = [
migrations.CreateModel(
name='ObjectPermission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('attrs', django.contrib.postgres.fields.jsonb.JSONField()),
('can_view', models.BooleanField(default=False)),
('can_add', models.BooleanField(default=False)),
('can_change', models.BooleanField(default=False)),
('can_delete', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, related_name='object_permissions', to='auth.Group')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('users', models.ManyToManyField(blank=True, related_name='object_permissions', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('model', 'attrs')},
},
),
]

View File

@@ -1,8 +1,10 @@
import binascii
import os
from django.contrib.auth.models import 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.core.validators import MinLengthValidator
from django.db import models
from django.db.models.signals import post_save
@@ -190,3 +192,54 @@ class Token(models.Model):
if self.expires is None or timezone.now() < self.expires:
return False
return True
class ObjectPermission(models.Model):
"""
A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
identified by ORM query parameters.
"""
users = models.ManyToManyField(
to=User,
blank=True,
related_name='object_permissions'
)
groups = models.ManyToManyField(
to=Group,
blank=True,
related_name='object_permissions'
)
model = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
)
attrs = JSONField(
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
)
class Meta:
unique_together = ('model', 'attrs')
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.
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

@@ -0,0 +1,62 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission, User
from django.test import TestCase, override_settings
from dcim.models import Site
from tenancy.models import Tenant
from users.models import ObjectPermission
class UserConfigTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser')
@classmethod
def setUpTestData(cls):
tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
Site.objects.bulk_create((
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2', tenant=tenant),
Site(name='Site 3', slug='site-3'),
))
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
def test_permission_view_object(self):
# Sanity check to ensure the user has no model-level permission
self.assertFalse(self.user.has_perm('dcim.view_site'))
# The permission check for a specific object should fail.
sites = Site.objects.all()
self.assertFalse(self.user.has_perm('dcim.view_site', sites[0]))
# Create and assign a new ObjectPermission specifying the first site by name.
ct = ContentType.objects.get_for_model(sites[0])
object_perm = ObjectPermission(
model=ct,
attrs={'name': 'Site 1'},
can_view=True
)
object_perm.save()
self.user.object_permissions.add(object_perm)
# The test user should have permission to view only the first site.
self.assertTrue(self.user.has_perm('dcim.view_site', sites[0]))
self.assertFalse(self.user.has_perm('dcim.view_site', sites[1]))
# Create a second ObjectPermission matching sites by assigned tenant.
object_perm = ObjectPermission(
model=ct,
attrs={'tenant__name': 'Tenant 1'},
can_view=True
)
object_perm.save()
self.user.object_permissions.add(object_perm)
# The user should now able to view the first two sites, but not the third.
self.assertTrue(self.user.has_perm('dcim.view_site', sites[0]))
self.assertTrue(self.user.has_perm('dcim.view_site', sites[1]))
self.assertFalse(self.user.has_perm('dcim.view_site', sites[2]))