Skip to content

Commit dd52e89

Browse files
committed
Add Permission Checks for Promgen Web
Create a mixin class to check permission logic: - For Service/Farm, the object being checked is itself. - For Project, the object being checked is itself or its parent Service. - For Host, the object being checked is its parent Farm. - For Exporter/URL, the object being checked is its parent Project or the Service that is the parent of the Project. - For Rule/Sender, the object being checked is its parent Service/Project. - Other cases only have permission if the user being checked is a superuser. Apply the mixin class to View classes: - 'View' classes (List, Detail) are not applied. - The 'ServiceRegister' class is not applied. - 'Update' classes require the user to have EDIT or MANAGE permissions. - 'Delete' classes require the user to have MANAGE permissions.
1 parent 8db914b commit dd52e89

File tree

6 files changed

+308
-39
lines changed

6 files changed

+308
-39
lines changed

promgen/mixins.py

+67-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Copyright (c) 2019 LINE Corporation
22
# These sources are released under the terms of the MIT license: see LICENSE
3-
3+
import guardian.mixins
4+
import guardian.utils
45
from django.contrib import messages
56
from django.contrib.auth.mixins import PermissionRequiredMixin
67
from django.contrib.auth.views import redirect_to_login
78
from django.contrib.contenttypes.models import ContentType
8-
from django.shortcuts import get_object_or_404
9+
from django.shortcuts import get_object_or_404, redirect
910
from django.views.generic.base import ContextMixin
1011

1112
from promgen import models
@@ -78,3 +79,67 @@ def get_context_data(self, **kwargs):
7879
models.Service, id=self.kwargs["pk"]
7980
)
8081
return context
82+
83+
84+
class PromgenGuardianPermissionMixin(guardian.mixins.PermissionRequiredMixin):
85+
86+
def get_check_permission_object(self):
87+
# Override this method to return the object to check permissions for
88+
return self.get_object()
89+
90+
def get_check_permission_objects(self):
91+
# We only define permission for Service/Project/Farm
92+
# So we need to check the permission for the parent objects in other cases
93+
try:
94+
object = self.get_check_permission_object()
95+
if isinstance(object, models.Farm):
96+
return [object]
97+
elif isinstance(object, models.Host):
98+
return [object, object.farm]
99+
elif isinstance(object, models.Service):
100+
return [object]
101+
elif isinstance(object, models.Project):
102+
return [object, object.service]
103+
elif isinstance(object, models.Exporter) or isinstance(object, models.URL):
104+
return [object.project, object.project.service]
105+
elif isinstance(object, models.Rule) or isinstance(object, models.Sender):
106+
if isinstance(object.content_object, models.Project):
107+
return [object.content_object, object.content_object.service]
108+
else:
109+
return [object.content_object]
110+
except:
111+
return None
112+
113+
def check_permissions(self, request):
114+
check_permission_objects = self.get_check_permission_objects()
115+
if check_permission_objects is None:
116+
if request.user.is_active and request.user.is_superuser:
117+
return None
118+
return self.on_permission_check_fail(request, None)
119+
# Loop through all the objects to check permissions for
120+
# If any of the objects has the required permission (any_perm=True), we can proceed
121+
# Otherwise, we will return the forbidden response
122+
forbidden = None
123+
for obj in check_permission_objects:
124+
forbidden = guardian.utils.get_40x_or_None(request,
125+
perms=self.get_required_permissions(
126+
request),
127+
obj=obj,
128+
login_url=self.login_url,
129+
redirect_field_name=self.redirect_field_name,
130+
return_403=self.return_403,
131+
return_404=self.return_404,
132+
accept_global_perms=False,
133+
any_perm=True,
134+
)
135+
if forbidden is None:
136+
break
137+
if forbidden:
138+
return self.on_permission_check_fail(request, forbidden)
139+
140+
def on_permission_check_fail(self, request, response, obj=None):
141+
messages.warning(request, "You do not have permission to perform this action.")
142+
referer = request.META.get("HTTP_REFERER")
143+
if referer:
144+
return redirect(referer)
145+
return redirect_to_login(self.request.get_full_path())

promgen/tests/test_host_add.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Copyright (c) 2017 LINE Corporation
22
# These sources are released under the terms of the MIT license: see LICENSE
3-
4-
3+
from django.shortcuts import get_object_or_404
54
from django.urls import reverse
5+
from guardian.shortcuts import assign_perm
66

77
from promgen import models, validators
8+
from promgen.middleware import get_current_user
89
from promgen.tests import PromgenTest
910

1011

@@ -16,6 +17,7 @@ def setUp(self):
1617
# separated and comma separated work, but are not necessarily testing
1718
# valid/invalid hostnames
1819
def test_newline(self):
20+
assign_perm("promgen.edit_farm", get_current_user(), get_object_or_404(models.Farm, pk=1))
1921
self.client.post(
2022
reverse("hosts-add", args=[1]),
2123
{"hosts": "\naaa.example.com\nbbb.example.com\nccc.example.com \n"},
@@ -24,6 +26,7 @@ def test_newline(self):
2426
self.assertCount(models.Host, 3, "Expected 3 hosts")
2527

2628
def test_comma(self):
29+
assign_perm("promgen.edit_farm", get_current_user(), get_object_or_404(models.Farm, pk=1))
2730
self.client.post(
2831
reverse("hosts-add", args=[1]),
2932
{"hosts": ",,aaa.example.com, bbb.example.com,ccc.example.com,"},

promgen/tests/test_mixins.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) 2025 LINE Corporation
2+
# These sources are released under the terms of the MIT license: see LICENSE
3+
from unittest.mock import patch
4+
5+
from django.shortcuts import get_object_or_404
6+
from django.test import RequestFactory
7+
from guardian.shortcuts import assign_perm
8+
9+
from promgen import models
10+
from promgen import tests
11+
from promgen.mixins import PromgenGuardianPermissionMixin
12+
13+
14+
class MockView(PromgenGuardianPermissionMixin):
15+
def get_object(self):
16+
return self.object
17+
18+
def dispatch(self, request, *args, **kwargs):
19+
self.request = request
20+
response = self.check_permissions(request)
21+
if response:
22+
return "Permission Denied"
23+
return "Permission Granted"
24+
25+
26+
class PromgenGuardianPermissionMixinTest(tests.PromgenTest):
27+
def setUp(self):
28+
self.view = MockView()
29+
factory = RequestFactory()
30+
self.request = factory.get("/example-url")
31+
32+
def test_permission_granted(self):
33+
user = self.force_login(username="demo")
34+
object = get_object_or_404(models.Project, pk=1)
35+
permission_required = "manage_project"
36+
assign_perm(permission_required, user, object)
37+
self.view.permission_required = permission_required
38+
self.view.object = object
39+
self.request.user = user
40+
response = self.view.dispatch(self.request)
41+
self.assertEqual(response, "Permission Granted")
42+
43+
@patch("django.contrib.messages.api.add_message")
44+
def test_permission_not_granted(self, mock_add_message):
45+
user = self.force_login(username="demo")
46+
object = get_object_or_404(models.Project, pk=1)
47+
permission_required = "manage_project"
48+
self.view.permission_required = permission_required
49+
self.view.object = object
50+
self.request.user = user
51+
response = self.view.dispatch(self.request)
52+
self.assertEqual(response, "Permission Denied")
53+
54+
def test_permission_granted_on_parent_object(self):
55+
user = self.force_login(username="demo")
56+
object = get_object_or_404(models.Service, pk=1)
57+
permission_required = "manage_service"
58+
assign_perm(permission_required, user, object)
59+
self.view.permission_required = permission_required
60+
self.view.object = object
61+
self.request.user = user
62+
response = self.view.dispatch(self.request)
63+
self.assertEqual(response, "Permission Granted")
64+
65+
@patch("django.contrib.messages.api.add_message")
66+
def test_permission_granted_on_another_object(self, mock_add_message):
67+
user = self.force_login(username="demo")
68+
object = get_object_or_404(models.Service, pk=1)
69+
another_object = models.Service.objects.create(name="Another Service")
70+
permission_required = "manage_service"
71+
assign_perm(permission_required, user, another_object)
72+
self.view.permission_required = permission_required
73+
self.view.object = object
74+
self.request.user = user
75+
response = self.view.dispatch(self.request)
76+
self.assertEqual(response, "Permission Denied")

promgen/tests/test_routes.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.urls import reverse
88

99
from promgen import models, tests, views
10+
from promgen.middleware import get_current_user
1011

1112
TEST_SETTINGS = tests.Data("examples", "promgen.yml").yaml()
1213
TEST_IMPORT = tests.Data("examples", "import.json").raw()
@@ -104,7 +105,11 @@ def test_failed_permission(self):
104105
self.assertTrue(response.url.startswith("/login"))
105106

106107
def test_other_routes(self):
107-
self.add_user_permissions("promgen.add_rule", "promgen.change_site")
108+
user = get_current_user()
109+
user.is_superuser = True
110+
user.save()
108111
for request in [{"viewname": "rule-new", "args": ("site", 1)}]:
109112
response = self.client.get(reverse(**request))
110113
self.assertRoute(response, views.AlertRuleRegister, 200)
114+
user.is_superuser = False
115+
user.save()

promgen/tests/test_web.py

+40-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Copyright (c) 2022 LINE Corporation
22
# These sources are released under the terms of the MIT license: see LICENSE
33
from django.urls import reverse
4+
from guardian.shortcuts import assign_perm, remove_perm
45

5-
from promgen import tests, views
6+
from promgen import views, models
7+
from promgen.tests import PromgenTest
68

79

8-
class WebTests(tests.PromgenTest):
10+
class WebTests(PromgenTest):
911
fixtures = ["testcases.yaml", "extras.yaml"]
1012

1113
route_map = [
@@ -15,9 +17,31 @@ class WebTests(tests.PromgenTest):
1517
("service-list", views.ServiceList, {}),
1618
("service-detail", views.ServiceDetail, {"pk": 1}),
1719
("project-detail", views.ProjectDetail, {"pk": 1}),
18-
("farm-link", views.FarmLink, {"pk": 1, "source": "promgen"}),
19-
("project-exporter", views.ExporterRegister, {"pk": 1}),
20-
("project-notifier", views.ProjectNotifierRegister, {"pk": 1}),
20+
("farm-link", views.FarmLink,
21+
{
22+
"pk": 1,
23+
"source": "promgen",
24+
"permission": "edit_project",
25+
"model": models.Project,
26+
"permission_object_pk": 1
27+
}
28+
),
29+
("project-exporter", views.ExporterRegister,
30+
{
31+
"pk": 1,
32+
"permission": "edit_project",
33+
"model": models.Project,
34+
"permission_object_pk": 1
35+
}
36+
),
37+
("project-notifier", views.ProjectNotifierRegister,
38+
{
39+
"pk": 1,
40+
"permission": "edit_project",
41+
"model": models.Project,
42+
"permission_object_pk": 1
43+
}
44+
),
2145
("url-list", views.URLList, {}),
2246
("farm-list", views.FarmList, {}),
2347
("farm-detail", views.FarmDetail, {"pk": 1}),
@@ -40,6 +64,13 @@ def setUp(self):
4064

4165
def test_routes(self):
4266
for viewname, viewclass, params in self.route_map:
67+
permission = params.pop("permission", None)
68+
permission_model = params.pop("model", None)
69+
permission_object_pk = params.pop("permission_object_pk", None)
70+
if permission and permission_model and permission_object_pk:
71+
permission_object = permission_model.objects.get(pk=permission_object_pk)
72+
assign_perm(permission, self.user, permission_object)
73+
4374
# By default we'll pass all params as-is to our reverse()
4475
# method, but we may have a few special ones (like status_code)
4576
# that we want to pop and handle separately
@@ -49,3 +80,7 @@ def test_routes(self):
4980
with self.subTest(viewname=viewname, params=params):
5081
response = self.client.get(reverse(viewname, kwargs=params))
5182
self.assertRoute(response, viewclass, status_code)
83+
84+
if permission and permission_model and permission_object_pk:
85+
permission_object = permission_model.objects.get(pk=permission_object_pk)
86+
remove_perm(permission, self.user, permission_object)

0 commit comments

Comments
 (0)