Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ffa7cae
Add Permission Checks for Promgen Web
hoangpn Feb 6, 2025
3c0466c
Remove the "Default" group
hoangpn May 8, 2025
fe81ac6
Replace the DRF's default permission class
hoangpn May 13, 2025
84bf454
Filter the Services on Home page by the user's permissions
hoangpn Apr 15, 2025
441ad45
Filter the Service list by the user's permissions
hoangpn Apr 22, 2025
28bc339
Filter the Rule list by the user's permissions
hoangpn Apr 22, 2025
7cd5bb9
Filter the Farm list by the user's permissions
hoangpn Apr 22, 2025
d12301d
Filter the URL list by the user's permissions
hoangpn Apr 22, 2025
3f62a05
Filter the Host list by the user's permissions
hoangpn Apr 22, 2025
808f6db
Filter the Alert History by the user's permissions
hoangpn Apr 22, 2025
f934a14
Filter the Edit History by the user's permissions
hoangpn Apr 22, 2025
8c52581
Filter the Search result by the user's permissions
hoangpn Apr 22, 2025
b950c36
Filter the Project list of the Datasource by the user's permissions
hoangpn Apr 23, 2025
4730f06
Filter the Host page by the user's permissions
hoangpn Apr 23, 2025
284bd56
Filter the Proxy's Alerts and Proxy's Silences by the user's permissions
hoangpn Apr 23, 2025
125b7a3
Filter the Service retrieve API by the user's permissions
hoangpn May 13, 2025
5912437
Filter the Project retrieve API by the user's permissions
hoangpn May 13, 2025
f052160
Filter the Farm retrieve API by the user's permissions
hoangpn May 13, 2025
188efab
Filter the Export Rules API by the user's permissions
hoangpn May 13, 2025
9a594cd
Filter the Export Targets API by the user's permissions
hoangpn May 13, 2025
4f57ccc
Filter the Export URLs API by the user's permissions
hoangpn May 13, 2025
076cc6e
Check user's permissions when silencing an alert
hoangpn May 13, 2025
80f2c89
Check user's permissions when expiring a silence
hoangpn May 13, 2025
b62cd1f
Filter the Group list by user's permissions
hoangpn Sep 30, 2025
d011aef
Remove the explanatory message of the permission block
hoangpn Oct 6, 2025
8193aac
Add check per-object permissions for ExporterScrape view
hoangpn Nov 5, 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/0040_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", "0039_migrate_user_type_sender_to_pk"),
]

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