From 74a5024593646bf5895518be99783ab8f08078a7 Mon Sep 17 00:00:00 2001 From: hoang Date: Thu, 6 Feb 2025 16:52:52 +0700 Subject: [PATCH] Add 'Permissions' panel to UI Add a panel to the UI of each detail screen of Service/Project/Farm object to assign or remove permissions for users. User needs to have MANAGE permissions to assign or remove permissions for users through this panel. --- promgen/forms.py | 37 +++++++++++ promgen/templates/promgen/farm_detail.html | 5 ++ .../templates/promgen/permission_block.html | 38 +++++++++++ promgen/templates/promgen/permission_row.html | 26 ++++++++ promgen/templates/promgen/project_detail.html | 1 + .../promgen/project_detail_permissions.html | 6 ++ promgen/templates/promgen/service_detail.html | 8 +++ promgen/templatetags/promgen.py | 7 ++ promgen/urls.py | 3 + promgen/views.py | 64 ++++++++++++++++++- 10 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 promgen/templates/promgen/permission_block.html create mode 100644 promgen/templates/promgen/permission_row.html create mode 100644 promgen/templates/promgen/project_detail_permissions.html diff --git a/promgen/forms.py b/promgen/forms.py index 857213431..71dc6f349 100644 --- a/promgen/forms.py +++ b/promgen/forms.py @@ -6,7 +6,9 @@ from dateutil import parser from django import forms +from django.contrib.auth.models import User from django.core.exceptions import ValidationError +from guardian.shortcuts import get_perms_for_model from promgen import errors, models, plugins, prometheus, validators @@ -254,3 +256,38 @@ def clean(self): if not hosts: raise ValidationError("No valid hosts") self.cleaned_data["hosts"] = list(hosts) + + +class UserPermissionForm(forms.Form): + permission = forms.ChoiceField( + required=True, + label="Permission", + ) + + username = forms.ChoiceField( + required=True, + label="Username", + ) + + def __init__(self, *args, **kwargs): + input_object = kwargs.pop("input_object", None) + super(UserPermissionForm, self).__init__(*args, **kwargs) + if input_object: + self.fields["permission"].choices = self.get_permission_choices(input_object) + self.fields["username"].choices = self.get_user_choices() + + def get_permission_choices(self, input_object): + permissions = get_perms_for_model(input_object) + for permission in permissions: + yield (permission.codename, permission.name) + + def get_user_choices(self): + for u in (User.objects.filter(is_active=True, is_superuser=False) + .exclude(username="AnonymousUser") + .order_by("username")): + if u.first_name: + yield (u.username, f"{u.username} ({u.first_name} {u.last_name})") + elif u.email: + yield (u.username, f"{u.username} ({u.email})") + else: + yield (u.username, u.username) diff --git a/promgen/templates/promgen/farm_detail.html b/promgen/templates/promgen/farm_detail.html index f0ca74ed6..c8914470d 100644 --- a/promgen/templates/promgen/farm_detail.html +++ b/promgen/templates/promgen/farm_detail.html @@ -71,4 +71,9 @@

Farm: {{ farm.name }} ({{ farm.source }}) +
+
Permissions
+ {% include "promgen/permission_block.html" with object=farm %} +
+ {% endblock %} diff --git a/promgen/templates/promgen/permission_block.html b/promgen/templates/promgen/permission_block.html new file mode 100644 index 000000000..cbb0b63ab --- /dev/null +++ b/promgen/templates/promgen/permission_block.html @@ -0,0 +1,38 @@ +{% load i18n %} +{% load promgen %} + + + + + + + + + {% for user,perms in object|get_users_permissions %} + {% include 'promgen/permission_row.html' %} + {% endfor %} +
UserEmail +
+ Permission +
+
Actions
+ + + diff --git a/promgen/templates/promgen/permission_row.html b/promgen/templates/promgen/permission_row.html new file mode 100644 index 000000000..43089deaa --- /dev/null +++ b/promgen/templates/promgen/permission_row.html @@ -0,0 +1,26 @@ +{% load i18n %} +{% load promgen %} + + {{ user.username }} + {{ user.email }} + + {% for perm in perms %} + {{ perm|upper }} + {% endfor %} + + +
+ {% csrf_token %} + + + + + +
+ + + + + + diff --git a/promgen/templates/promgen/project_detail.html b/promgen/templates/promgen/project_detail.html index 6842466bf..0e43f2776 100644 --- a/promgen/templates/promgen/project_detail.html +++ b/promgen/templates/promgen/project_detail.html @@ -66,5 +66,6 @@

{% include "promgen/project_detail_rules.html" %} +{% include "promgen/project_detail_permissions.html" %} {% endblock %} diff --git a/promgen/templates/promgen/project_detail_permissions.html b/promgen/templates/promgen/project_detail_permissions.html new file mode 100644 index 000000000..8b4e07130 --- /dev/null +++ b/promgen/templates/promgen/project_detail_permissions.html @@ -0,0 +1,6 @@ +{% load i18n %} + +
+
Permissions
+ {% include "promgen/permission_block.html" with object=project %} +
diff --git a/promgen/templates/promgen/service_detail.html b/promgen/templates/promgen/service_detail.html index 361aaad07..f2e3a46fd 100644 --- a/promgen/templates/promgen/service_detail.html +++ b/promgen/templates/promgen/service_detail.html @@ -25,6 +25,7 @@

Service: {{ service.name }}

  • Rules
  • Notifiers
  • +
  • Permissions
  • @@ -51,6 +52,13 @@

    Service: {{ service.name }}

    {% include "promgen/service_block_panel_notifiers.inc.html" %}
    + +
    +
    + {% include "promgen/permission_block.html" with object=service %} +
    +
    + diff --git a/promgen/templatetags/promgen.py b/promgen/templatetags/promgen.py index 3a1c2c157..eb371934b 100644 --- a/promgen/templatetags/promgen.py +++ b/promgen/templatetags/promgen.py @@ -14,6 +14,7 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ +from guardian.shortcuts import get_users_with_perms register = template.Library() @@ -214,3 +215,9 @@ def urlqs(view, **kwargs): This only works for views that do not need additional parameters """ return reverse(view) + "?" + urlencode(kwargs) + + +@register.filter() +def get_users_permissions(object): + return get_users_with_perms(object, attach_perms=True, with_superusers=False, + with_group_users=False).items() diff --git a/promgen/urls.py b/promgen/urls.py index 70b69252f..8968e2b41 100644 --- a/promgen/urls.py +++ b/promgen/urls.py @@ -87,6 +87,9 @@ path("rule//toggle", views.RuleToggle.as_view(), name="rule-toggle"), path("rule//test", csrf_exempt(views.RuleTest.as_view()), name="rule-test"), path("rule//duplicate", views.RulesCopy.as_view(), name="rule-overwrite"), + # Permissions + path("permission/assign", views.PermissionAssign.as_view(), name="permission-assign"), + path("permission/delete", views.PermissionDelete.as_view(), name="permission-delete"), # Generic Rules path("//rule", views.AlertRuleRegister.as_view(), name="rule-new"), # Other miscellaneous diff --git a/promgen/views.py b/promgen/views.py index 0ec9a4d37..46d521925 100644 --- a/promgen/views.py +++ b/promgen/views.py @@ -45,7 +45,7 @@ tasks, util, ) -from promgen.forms import UserPermForm +from promgen.forms import UserPermissionForm from promgen.mixins import PromgenGuardianPermissionMixin from promgen.shortcuts import resolve_domain @@ -286,6 +286,10 @@ class ServiceDetail(LoginRequiredMixin, DetailView): "project_set__notifiers__owner", ) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["permission_form"] = UserPermissionForm(input_object=self.object) + return context class ServiceDelete(PromgenGuardianPermissionMixin, DeleteView): @@ -496,6 +500,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["sources"] = models.Farm.driver_set() context["url_form"] = forms.URLForm() + context["permission_form"] = UserPermissionForm(input_object=self.object) return context @@ -510,6 +515,10 @@ class FarmList(LoginRequiredMixin, ListView): class FarmDetail(LoginRequiredMixin, DetailView): model = models.Farm + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["permission_form"] = UserPermissionForm(input_object=self.object) + return context class FarmUpdate(PromgenGuardianPermissionMixin, UpdateView): @@ -1508,3 +1517,56 @@ def get(self, request): return util.proxy_error(response) return HttpResponse(response.content, content_type="application/json") + + +class PermissionAssign(PromgenGuardianPermissionMixin, View): + permission_required = ["manage_service", "manage_project", "manage_farm"] + + def post(self, request): + user = User.objects.get_by_natural_key(request.POST["username"]) + permission = request.POST["permission"] + obj = self.get_object() + + # User should only have one permission MANAGE or EDIT for an object + # So we remove all permissions before assigning new one + permissions = get_perms(user, obj) + for perm in permissions: + remove_perm(perm, user, obj) + + assign_perm(permission, user, obj) + messages.success( + request, + "Assigned permission: {} for user: {} on: {}".format(permission, user.username, + obj.name), + ) + return redirect(request.POST["next"]) + + def get_object(self): + id = self.request.POST["id"] + model = self.request.POST["model"] + models = ContentType.objects.get(app_label="promgen", model=model) + obj = models.get_object_for_this_type(pk=id) + return obj + + +class PermissionDelete(PromgenGuardianPermissionMixin, View): + permission_required = ["manage_service", "manage_project", "manage_farm"] + + def post(self, request): + user = User.objects.get_by_natural_key(request.POST["username"]) + obj = self.get_object() + permissions = get_perms(user, obj) + for perm in permissions: + remove_perm(perm, user, obj) + messages.success( + request, + "Removed all permissions of user: {} on: {}".format(user.username, obj.name), + ) + return redirect(request.POST["next"]) + + def get_object(self): + id = self.request.POST["id"] + model = self.request.POST["model"] + models = ContentType.objects.get(app_label="promgen", model=model) + obj = models.get_object_for_this_type(pk=id) + return obj