Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d58485
Add Permission Checks for Promgen Web
hoangpn Feb 6, 2025
5210e59
Remove the "Default" group
hoangpn May 8, 2025
46480b0
Replace the DRF's default permission class
hoangpn May 13, 2025
c9be79c
Filter the Services on Home page by the user's permissions
hoangpn Apr 15, 2025
944b519
Filter the Service list by the user's permissions
hoangpn Apr 22, 2025
e258997
Filter the Rule list by the user's permissions
hoangpn Apr 22, 2025
f4a11d6
Filter the Farm list by the user's permissions
hoangpn Apr 22, 2025
5a2ffd8
Filter the URL list by the user's permissions
hoangpn Apr 22, 2025
60c89f6
Filter the Host list by the user's permissions
hoangpn Apr 22, 2025
8debace
Filter the Alert History by the user's permissions
hoangpn Apr 22, 2025
fc1a8d0
Filter the Edit History by the user's permissions
hoangpn Apr 22, 2025
5e4b984
Filter the Search result by the user's permissions
hoangpn Apr 22, 2025
d6003b3
Filter the Project list of the Datasource by the user's permissions
hoangpn Apr 23, 2025
4a1486e
Filter the Host page by the user's permissions
hoangpn Apr 23, 2025
149d49f
Filter the Proxy's Alerts and Proxy's Silences by the user's permissions
hoangpn Apr 23, 2025
0ba53a2
Filter the Service retrieve API by the user's permissions
hoangpn May 13, 2025
795b68a
Filter the Project retrieve API by the user's permissions
hoangpn May 13, 2025
346aea2
Filter the Farm retrieve API by the user's permissions
hoangpn May 13, 2025
343f01e
Filter the Export Rules API by the user's permissions
hoangpn May 13, 2025
74f4934
Filter the Export Targets API by the user's permissions
hoangpn May 13, 2025
53c7903
Filter the Export URLs API by the user's permissions
hoangpn May 13, 2025
60fa215
Check user's permissions when silencing an alert
hoangpn May 13, 2025
9141134
Check user's permissions when expiring a silence
hoangpn May 13, 2025
b8e015d
Filter the Group list by user's permissions
hoangpn Sep 30, 2025
69ff77f
Remove the explanatory message of the permission block
hoangpn Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions promgen/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from dateutil import parser
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from guardian.conf.settings import ANONYMOUS_USER_NAME
Expand Down Expand Up @@ -268,7 +267,7 @@ def get_permission_choices(input_object):

def get_group_choices():
yield ("", "")
for g in models.Group.objects.exclude(name=settings.PROMGEN_DEFAULT_GROUP).order_by("name"):
for g in models.Group.objects.order_by("name"):
yield (g.name, g.name)


Expand Down
Binary file modified promgen/locale/ja/LC_MESSAGES/django.mo
Binary file not shown.
6 changes: 0 additions & 6 deletions promgen/locale/ja/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,6 @@ msgstr "Actions"
msgid "No search parameters provided."
msgstr "検索フレーズが指定されていません"

#: templates/promgen/service_detail.html:93
#: templates/promgen/project_detail.html:101
#: templates/promgen/farm_detail.html:71
msgid "This is a transitional release. Even if you are able to assign roles to members, the permission checks are actually disabled. They will be enabled in the next release."
msgstr "このリリースは移行用のリリースです。ロールをメンバーに割り当てても、権限のチェックは行われません。次のリリースで権限のチェックが有効化されます。"

#: templates/promgen/permission_row.html:28
msgid "Delete all permissions for user?"
msgstr "ユーザーから全ての権限を削除しますか?"
Expand Down
2 changes: 1 addition & 1 deletion promgen/migrations/0003_default-group.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


def create_group(apps, schema_editor):
if not settings.PROMGEN_DEFAULT_GROUP:
if not getattr(settings, "PROMGEN_DEFAULT_GROUP", None):
return

# Create Default Group
Expand Down
24 changes: 24 additions & 0 deletions promgen/migrations/0038_remove_default_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.11 on 2025-08-14 08:34

from django.db import migrations


def remove_group(apps, schema_editor):
# Get the Default group
# Note: The group name is hardcoded as "Default" in the original Promgen.
# If the name is different in your application, you should change it
# according to the value used in settings.PROMGEN_DEFAULT_GROUP.
default_group = apps.get_model("auth", "Group").objects.filter(name="Default").first()

if default_group:
default_group.user_set.clear()
default_group.permissions.clear()
default_group.delete()


class Migration(migrations.Migration):
dependencies = [
("promgen", "0037_remove_project_farm_alter_farm_project"),
]

operations = [migrations.RunPython(remove_group)]
86 changes: 83 additions & 3 deletions promgen/mixins.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Copyright (c) 2019 LINE Corporation
# These sources are released under the terms of the MIT license: see LICENSE

import guardian.mixins
import guardian.utils
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import redirect_to_login
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.views.generic.base import ContextMixin

from promgen import models
from promgen import models, views


class ContentTypeMixin:
Expand Down Expand Up @@ -78,3 +80,81 @@ def get_context_data(self, **kwargs):
models.Service, id=self.kwargs["pk"]
)
return context


class PromgenGuardianPermissionMixin(guardian.mixins.PermissionRequiredMixin):
def get_check_permission_object(self):
# Override this method to return the object to check permissions for
return self.get_object()

def get_check_permission_objects(self):
# We only define permission codes for Service, Group and Project
# So we need to check the permission for the parent objects in other cases
try:
object = self.get_check_permission_object()
if isinstance(object, models.Service) or isinstance(object, models.Group):
return [object]
if isinstance(object, models.Project):
return [object, object.service]
if (
isinstance(object, models.Exporter)
or isinstance(object, models.URL)
or isinstance(object, models.Farm)
):
return [object.project, object.project.service]
if isinstance(object, models.Host):
return [object.farm.project, object.farm.project.service]
if isinstance(object, models.Rule) or isinstance(object, models.Sender):
if isinstance(object.content_object, models.Project):
return [object.content_object, object.content_object.service]
else:
return [object.content_object]
return None
except Exception:
return None

def check_permissions(self, request):
# Always allow user to view the site rule
if isinstance(self, views.RuleDetail) and isinstance(
self.get_check_permission_object().content_object, models.Site
):
return None

check_permission_objects = self.get_check_permission_objects()
if check_permission_objects is None:
if request.user.is_active and request.user.is_superuser:
return None
return self.on_permission_check_fail(request, None)

# Loop through all the objects to check permissions for
# If any of the objects has the required permission (any_perm=True), we can proceed
# Otherwise, we will return the forbidden response
forbidden = None
for obj in check_permission_objects:
# Users always have permission on themselves
if isinstance(obj, User) and request.user == obj:
break

forbidden = guardian.utils.get_40x_or_None(
request,
perms=self.get_required_permissions(request),
obj=obj,
login_url=self.login_url,
redirect_field_name=self.redirect_field_name,
return_403=self.return_403,
return_404=self.return_404,
accept_global_perms=False,
any_perm=True,
)
if forbidden is None:
break
if forbidden:
return self.on_permission_check_fail(request, forbidden)
return None

def on_permission_check_fail(self, request, response, obj=None):
messages.warning(request, "You do not have permission to perform this action.")
referer = request.META.get("HTTP_REFERER")
if referer:
return redirect(referer)
return redirect_to_login(self.request.get_full_path())
57 changes: 57 additions & 0 deletions promgen/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Copyright (c) 2025 LINE Corporation
# These sources are released under the terms of the MIT license: see LICENSE
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils.itercompat import is_iterable
from guardian.shortcuts import get_objects_for_user
from rest_framework import permissions
from rest_framework.permissions import BasePermission

from promgen import models


class PromgenModelPermissions(BasePermission):
"""
Expand Down Expand Up @@ -41,3 +47,54 @@ def has_permission(self, request, view):
return any(request.user.has_perm(perm) for perm in perm_list)
else:
return all(request.user.has_perm(perm) for perm in perm_list)


class ReadOnlyForAuthenticatedUserOrIsSuperuser(BasePermission):
"""
Customize Django REST Framework's base permission class to only allow read-only access for
authenticated users and full access for superusers.
"""

def has_permission(self, request, view):
if request.user.is_superuser:
return True
return bool(
request.user
and request.user.is_authenticated
and request.method in permissions.SAFE_METHODS
)


def get_accessible_services_for_user(user: User):
return get_objects_for_user(
user,
["service_admin", "service_editor", "service_viewer"],
any_perm=True,
use_groups=True,
accept_global_perms=False,
klass=models.Service,
)


def get_accessible_projects_for_user(user: User):
services = get_accessible_services_for_user(user)
projects = get_objects_for_user(
user,
["project_admin", "project_editor", "project_viewer"],
any_perm=True,
use_groups=True,
accept_global_perms=False,
klass=models.Project,
)
return models.Project.objects.filter(Q(pk__in=projects) | Q(service__in=services))


def get_accessible_groups_for_user(user: User):
return get_objects_for_user(
user,
["group_admin", "group_member"],
any_perm=True,
use_groups=False,
accept_global_perms=False,
klass=models.Group,
)
21 changes: 15 additions & 6 deletions promgen/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,18 @@ def render_rules(rules=None):
return renderers.RuleRenderer().render(serializers.AlertRuleSerializer(rules, many=True).data)


def render_urls():
def render_urls(projects=None):
urls = collections.defaultdict(list)

for url in models.URL.objects.prefetch_related(
url_queryset = models.URL.objects.prefetch_related(
"project__service",
"project__shard",
"project",
):
)
if projects is not None:
url_queryset = url_queryset.filter(project__in=projects)

for url in url_queryset:
urls[
(
url.project.name,
Expand All @@ -98,7 +102,7 @@ def render_urls():
return json.dumps(data, indent=2, sort_keys=True)


def render_config(service=None, project=None):
def render_config(service=None, project=None, services=None, projects=None, farms=None):
data = []
for exporter in models.Exporter.objects.prefetch_related(
"project__farm__host_set",
Expand All @@ -113,6 +117,10 @@ def render_config(service=None, project=None):
continue
if project and exporter.project.name != project.name:
continue
if services is not None and exporter.project.service not in services:
continue
if projects is not None and exporter.project not in projects:
continue
if not exporter.enabled:
continue

Expand All @@ -129,8 +137,9 @@ def render_config(service=None, project=None):
labels["__metrics_path__"] = exporter.path

hosts = []
for host in exporter.project.farm.host_set.all():
hosts.append(f"{host.name}:{exporter.port}")
if farms is None or exporter.project.farm in farms:
for host in exporter.project.farm.host_set.all():
hosts.append(f"{host.name}:{exporter.port}")

data.append({"labels": labels, "targets": hosts})
return json.dumps(data, indent=2, sort_keys=True)
Expand Down
Loading