Add instance-level ownership
This commit is contained in:
parent
20d67f6f9f
commit
7e6bcf3e40
8 changed files with 228 additions and 1 deletions
|
@ -1,5 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth import get_permission_codename
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.http import HttpRequest
|
||||
|
||||
from redirect.models import Redirect
|
||||
|
||||
|
@ -10,5 +12,17 @@ from .models import RedirectUser
|
|||
class RedirectAdmin(admin.ModelAdmin):
|
||||
list_display = ["short_code", "target_url"]
|
||||
|
||||
def has_change_permission(self, request: HttpRequest, obj: Redirect = None):
|
||||
opts = self.opts
|
||||
codename = get_permission_codename("change", opts)
|
||||
return request.user.has_perm(f"{opts.app_label}.{codename}", obj)
|
||||
|
||||
def has_view_permission(self, request: HttpRequest, obj: Redirect = None):
|
||||
opts = self.opts
|
||||
codename = get_permission_codename("view", opts)
|
||||
return request.user.has_perm(
|
||||
f"{opts.app_label}.{codename}", obj
|
||||
) or self.has_change_permission(request, obj)
|
||||
|
||||
|
||||
admin.site.register(RedirectUser, UserAdmin)
|
||||
|
|
14
src/redirect/backends.py
Normal file
14
src/redirect/backends.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.contrib.auth.backends import BaseBackend, ModelBackend
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
from redirect.models import Redirect
|
||||
|
||||
|
||||
class RedirectBackend(ModelBackend):
|
||||
def has_perm(self, user_obj: AbstractUser, perm, obj=None):
|
||||
allowed = super().has_perm(user_obj, perm)
|
||||
if allowed:
|
||||
return allowed
|
||||
if obj and isinstance(obj, Redirect):
|
||||
return obj.owner == user_obj or obj.group_owner in user_obj.groups.all()
|
||||
return False
|
36
src/redirect/migrations/0003_auto_20220227_2216.py
Normal file
36
src/redirect/migrations/0003_auto_20220227_2216.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-27 21:16
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("redirect", "0002_alter_redirect_target_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="redirect",
|
||||
name="group_owner",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to="auth.group",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="redirect",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
38
src/redirect/migrations/0004_auto_20220227_2241.py
Normal file
38
src/redirect/migrations/0004_auto_20220227_2241.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-27 21:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("redirect", "0003_auto_20220227_2216"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="redirect",
|
||||
name="group_owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to="auth.group",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="redirect",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
40
src/redirect/migrations/0005_auto_20220227_2307.py
Normal file
40
src/redirect/migrations/0005_auto_20220227_2307.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-27 22:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("redirect", "0004_auto_20220227_2241"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="redirect",
|
||||
name="group_owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Give any group full permissions over this object.The user will still at least need global view permissions for the admin to work.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to="auth.group",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="redirect",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Give any user full permissions over this object. The user will still at least need global view permissions for the admin to work.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,10 +1,31 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Redirect(models.Model):
|
||||
short_code = models.CharField(max_length=250, blank=False, null=False, unique=True)
|
||||
target_url = models.URLField(max_length=2000)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Give any user full permissions over this object. "
|
||||
"The user will still at least need global view permissions "
|
||||
"for the admin to work.",
|
||||
)
|
||||
group_owner = models.ForeignKey(
|
||||
Group,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="owned_redirects",
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Give any group full permissions over this object."
|
||||
"The user will still at least need global view permissions "
|
||||
"for the admin to work.",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.short_code} => {self.target_url}"
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from django.contrib.auth.models import AbstractUser, Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import IntegrityError
|
||||
from django.test import TestCase
|
||||
from model_bakery import baker
|
||||
|
||||
from redirect.models import Redirect, RedirectUser
|
||||
|
||||
|
||||
class RedirectModelTestCase(TestCase):
|
||||
def test_has_short_code(self):
|
||||
|
@ -14,3 +18,62 @@ class RedirectModelTestCase(TestCase):
|
|||
|
||||
def test_has_target_url(self):
|
||||
baker.make("Redirect", target_url="https://static.augendre.info/potain3")
|
||||
|
||||
def test_can_miss_owner(self):
|
||||
baker.make("Redirect", owner=None)
|
||||
|
||||
def test_can_have_owner(self):
|
||||
user = RedirectUser.objects.create_user("user")
|
||||
baker.make("Redirect", owner=user)
|
||||
|
||||
def test_can_miss_group_owner(self):
|
||||
baker.make("Redirect", group_owner=None)
|
||||
|
||||
def test_can_have_group_owner(self):
|
||||
group = Group.objects.create(name="group")
|
||||
baker.make("Redirect", group_owner=group)
|
||||
|
||||
|
||||
class RedirectModelPermissionsTestMixin:
|
||||
def test_owner_can_change_object(self):
|
||||
assert self.owner.has_perm("redirect.change_redirect", self.redirect)
|
||||
|
||||
def test_owner_can_delete_object(self):
|
||||
assert self.owner.has_perm("redirect.delete_redirect", self.redirect)
|
||||
|
||||
def test_owner_can_view_object(self):
|
||||
assert self.owner.has_perm("redirect.view_redirect", self.redirect)
|
||||
|
||||
def test_non_owner_cant_change_object(self):
|
||||
assert not self.non_owner.has_perm("redirect.change_redirect", self.redirect)
|
||||
|
||||
def test_non_owner_cant_delete_object(self):
|
||||
assert not self.non_owner.has_perm("redirect.delete_redirect", self.redirect)
|
||||
|
||||
|
||||
class RedirectModelOwnerPermissionsTestCase(
|
||||
RedirectModelPermissionsTestMixin, TestCase
|
||||
):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.owner: AbstractUser = RedirectUser.objects.create_user("owner")
|
||||
cls.non_owner: AbstractUser = RedirectUser.objects.create_user("rando")
|
||||
content_type = ContentType.objects.get_for_model(Redirect)
|
||||
permission = Permission.objects.get(
|
||||
codename="view_redirect",
|
||||
content_type=content_type,
|
||||
)
|
||||
cls.non_owner.user_permissions.add(permission)
|
||||
cls.redirect = baker.make("Redirect", owner=cls.owner)
|
||||
|
||||
|
||||
class RedirectModelGroupOwnerPermissionsTestCase(
|
||||
RedirectModelPermissionsTestMixin, TestCase
|
||||
):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.owner: AbstractUser = RedirectUser.objects.create_user("owner")
|
||||
cls.non_owner: AbstractUser = RedirectUser.objects.create_user("rando")
|
||||
group_owner = Group.objects.create(name="group_owner")
|
||||
group_owner.user_set.add(cls.owner)
|
||||
cls.redirect = baker.make("Redirect", group_owner=group_owner)
|
||||
|
|
|
@ -86,6 +86,7 @@ TEMPLATES = [
|
|||
|
||||
WSGI_APPLICATION = "shortener.wsgi.application"
|
||||
|
||||
AUTHENTICATION_BACKENDS = ["redirect.backends.RedirectBackend"]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
|
Loading…
Reference in a new issue