feat: scope project access to Django Admin
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.products.access import accessible_projects, can_access_project
|
||||
from apps.products.models import (
|
||||
Article, MainProduct, ProjectAccess, ProjectAccessPolicy, SubProduct,
|
||||
)
|
||||
|
||||
|
||||
class ProjectAdminAccessTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.owner = User.objects.create_user(username="owner", password="password123", is_staff=True)
|
||||
self.other = User.objects.create_user(username="other", password="password123", is_staff=True)
|
||||
self.admin = User.objects.create_superuser(username="superadmin", password="password123")
|
||||
self.main = MainProduct.objects.create(
|
||||
created_by=self.owner, name="Private Alpha", slug="private-alpha",
|
||||
short_description="Alpha summary", description="Alpha details",
|
||||
)
|
||||
self.sub = SubProduct.objects.create(
|
||||
created_by=self.owner, main_product=self.main, name="Private Module",
|
||||
slug="private-module", short_description="Module summary", description="Module details",
|
||||
)
|
||||
self.article = Article.objects.create(main_product=self.main, title="Alpha Article", description="Article body")
|
||||
|
||||
def test_public_website_keeps_published_projects_visible_to_everyone(self):
|
||||
self.assertContains(self.client.get(reverse("products:overview")), self.main.name)
|
||||
self.assertContains(self.client.get(self.main.get_absolute_url()), self.main.name)
|
||||
self.assertContains(self.client.get(self.sub.get_absolute_url()), self.sub.name)
|
||||
self.client.force_login(self.other)
|
||||
self.assertContains(self.client.get(self.main.get_absolute_url()), self.main.name)
|
||||
self.assertContains(self.client.get(self.sub.get_absolute_url()), self.sub.name)
|
||||
|
||||
def test_default_admin_access_is_creator_only(self):
|
||||
self.assertTrue(can_access_project(self.owner, self.main, edit=True))
|
||||
self.assertTrue(can_access_project(self.owner, self.sub, edit=True))
|
||||
self.assertFalse(can_access_project(self.other, self.main))
|
||||
self.assertFalse(can_access_project(self.other, self.sub))
|
||||
self.client.force_login(self.other)
|
||||
self.assertNotContains(
|
||||
self.client.get(reverse("admin:products_mainproduct_changelist")), self.main.name
|
||||
)
|
||||
self.assertNotEqual(
|
||||
self.client.get(reverse("admin:products_mainproduct_change", args=[self.main.pk])).status_code,
|
||||
200,
|
||||
)
|
||||
|
||||
def test_no_projects_mode_removes_default_owner_access(self):
|
||||
ProjectAccessPolicy.objects.create(user=self.owner, mode=ProjectAccessPolicy.NONE)
|
||||
self.assertFalse(can_access_project(self.owner, self.main))
|
||||
self.assertFalse(can_access_project(self.owner, self.sub))
|
||||
self.client.force_login(self.owner)
|
||||
self.assertNotContains(
|
||||
self.client.get(reverse("admin:products_mainproduct_changelist")), self.main.name
|
||||
)
|
||||
self.assertNotEqual(
|
||||
self.client.get(reverse("admin:products_mainproduct_add")).status_code, 200
|
||||
)
|
||||
self.assertContains(self.client.get(self.main.get_absolute_url()), self.main.name)
|
||||
|
||||
def test_selected_view_and_edit_grants_are_independent(self):
|
||||
main_grant = ProjectAccess.objects.create(
|
||||
user=self.other, main_product=self.main, level=ProjectAccess.VIEW
|
||||
)
|
||||
self.assertTrue(can_access_project(self.other, self.main))
|
||||
self.assertFalse(can_access_project(self.other, self.main, edit=True))
|
||||
self.assertFalse(can_access_project(self.other, self.sub))
|
||||
|
||||
self.client.force_login(self.other)
|
||||
change_url = reverse("admin:products_mainproduct_change", args=[self.main.pk])
|
||||
response = self.client.get(change_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "View Main Project")
|
||||
self.assertEqual(self.client.post(change_url, {"name": "Tampered"}).status_code, 403)
|
||||
self.main.refresh_from_db()
|
||||
self.assertEqual(self.main.name, "Private Alpha")
|
||||
|
||||
main_grant.level = ProjectAccess.EDIT
|
||||
main_grant.save(update_fields=["level"])
|
||||
self.assertTrue(can_access_project(self.other, self.main, edit=True))
|
||||
self.assertEqual(self.client.get(change_url).status_code, 200)
|
||||
self.assertFalse(can_access_project(self.other, self.sub, edit=True))
|
||||
|
||||
def test_all_projects_modes_and_explicit_denials(self):
|
||||
policy = ProjectAccessPolicy.objects.create(user=self.other, mode=ProjectAccessPolicy.ALL_VIEW)
|
||||
self.assertTrue(can_access_project(self.other, self.main))
|
||||
self.assertFalse(can_access_project(self.other, self.main, edit=True))
|
||||
policy.mode = ProjectAccessPolicy.ALL_EDIT
|
||||
policy.save(update_fields=["mode"])
|
||||
self.assertTrue(can_access_project(self.other, self.main, edit=True))
|
||||
self.assertTrue(can_access_project(self.other, self.sub, edit=True))
|
||||
ProjectAccess.objects.create(user=self.other, main_product=self.main, level=ProjectAccess.NONE)
|
||||
self.assertFalse(can_access_project(self.other, self.main))
|
||||
self.assertTrue(can_access_project(self.other, self.sub, edit=True))
|
||||
self.assertTrue(can_access_project(self.admin, self.main, edit=True))
|
||||
|
||||
def test_explicit_view_override_can_remove_owners_edit_access(self):
|
||||
ProjectAccess.objects.create(user=self.owner, main_product=self.main, level=ProjectAccess.VIEW)
|
||||
self.assertTrue(can_access_project(self.owner, self.main))
|
||||
self.assertFalse(can_access_project(self.owner, self.main, edit=True))
|
||||
self.assertTrue(can_access_project(self.owner, self.sub, edit=True))
|
||||
|
||||
def test_subproject_grant_does_not_grant_parent_or_siblings_in_admin(self):
|
||||
ProjectAccess.objects.create(user=self.other, sub_product=self.sub, level=ProjectAccess.VIEW)
|
||||
self.client.force_login(self.other)
|
||||
self.assertContains(
|
||||
self.client.get(reverse("admin:products_subproduct_changelist")), self.sub.name
|
||||
)
|
||||
self.assertNotContains(
|
||||
self.client.get(reverse("admin:products_mainproduct_changelist")), self.main.name
|
||||
)
|
||||
self.assertContains(self.client.get(self.main.get_absolute_url()), self.main.name)
|
||||
|
||||
def test_related_article_admin_uses_parent_permission(self):
|
||||
ProjectAccess.objects.create(user=self.other, main_product=self.main, level=ProjectAccess.VIEW)
|
||||
self.client.force_login(self.other)
|
||||
change_url = reverse("admin:products_article_change", args=[self.article.pk])
|
||||
self.assertEqual(self.client.get(change_url).status_code, 200)
|
||||
self.assertEqual(self.client.post(change_url, {"title": "Tampered"}).status_code, 403)
|
||||
|
||||
def test_main_edit_does_not_expose_subproject_without_sub_edit_permission(self):
|
||||
ProjectAccess.objects.create(user=self.other, main_product=self.main, level=ProjectAccess.EDIT)
|
||||
ProjectAccess.objects.create(user=self.other, sub_product=self.sub, level=ProjectAccess.VIEW)
|
||||
self.client.force_login(self.other)
|
||||
response = self.client.get(reverse("admin:products_mainproduct_change", args=[self.main.pk]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'value="private-module"')
|
||||
sub_url = reverse("admin:products_subproduct_change", args=[self.sub.pk])
|
||||
self.assertContains(self.client.get(sub_url), "View Sub-project")
|
||||
self.assertEqual(self.client.post(sub_url, {"name": "Tampered"}).status_code, 403)
|
||||
|
||||
def test_admin_user_form_has_global_and_per_project_controls(self):
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.get(reverse("admin:auth_user_change", args=[self.other.pk]))
|
||||
self.assertContains(response, "project_access_policy-0-mode")
|
||||
self.assertContains(response, "project_access_grants-0-level")
|
||||
|
||||
def test_admin_project_creation_records_creator(self):
|
||||
self.client.force_login(self.other)
|
||||
response = self.client.post(reverse("admin:products_mainproduct_add"), {
|
||||
"name": "Admin Created", "slug": "admin-created", "short_description": "Summary",
|
||||
"description": "Details", "description_format": "plain", "order": 0,
|
||||
"homepage_order": 0,
|
||||
"people-TOTAL_FORMS": 0, "people-INITIAL_FORMS": 0,
|
||||
"articles-TOTAL_FORMS": 0, "articles-INITIAL_FORMS": 0,
|
||||
"videos-TOTAL_FORMS": 0, "videos-INITIAL_FORMS": 0,
|
||||
"sub_products-TOTAL_FORMS": 0, "sub_products-INITIAL_FORMS": 0,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(MainProduct.objects.get(slug="admin-created").created_by, self.other)
|
||||
|
||||
def test_override_must_target_one_project_and_cannot_duplicate(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
ProjectAccess(user=self.other).full_clean()
|
||||
with self.assertRaises(ValidationError):
|
||||
ProjectAccess(user=self.other, main_product=self.main, sub_product=self.sub).full_clean()
|
||||
ProjectAccess.objects.create(user=self.other, main_product=self.main, level=ProjectAccess.VIEW)
|
||||
with self.assertRaises(ValidationError):
|
||||
ProjectAccess(user=self.other, main_product=self.main, level=ProjectAccess.EDIT).full_clean()
|
||||
Reference in New Issue
Block a user