Initial django guardian integrations
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
|
||||
from .models import AccessRequest
|
||||
|
||||
|
||||
@admin.register(AccessRequest)
|
||||
class AccessRequestAdmin(admin.ModelAdmin):
|
||||
class AccessRequestAdmin(GuardedModelAdmin):
|
||||
list_display = (
|
||||
"id",
|
||||
"requester",
|
||||
|
||||
@@ -5,3 +5,7 @@ class AccessConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.access"
|
||||
verbose_name = "Access Requests"
|
||||
|
||||
def ready(self) -> None:
|
||||
from . import signals # noqa: F401
|
||||
return super().ready()
|
||||
|
||||
23
app/apps/access/signals.py
Normal file
23
app/apps/access/signals.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from apps.core.rbac import assign_default_object_permissions
|
||||
from .models import AccessRequest
|
||||
|
||||
|
||||
@receiver(post_save, sender=AccessRequest)
|
||||
def assign_access_request_perms(sender, instance: AccessRequest, created: bool, **kwargs) -> None:
|
||||
if not created:
|
||||
return
|
||||
if instance.requester_id:
|
||||
user = instance.requester
|
||||
for perm in (
|
||||
"access.view_accessrequest",
|
||||
"access.change_accessrequest",
|
||||
"access.delete_accessrequest",
|
||||
):
|
||||
assign_perm(perm, user, instance)
|
||||
assign_default_object_permissions(instance)
|
||||
@@ -11,10 +11,11 @@ class CoreConfig(AppConfig):
|
||||
verbose_name = "Core"
|
||||
|
||||
def ready(self) -> None:
|
||||
from .rbac import ensure_role_groups
|
||||
from .rbac import assign_role_permissions, ensure_role_groups
|
||||
|
||||
def _ensure_roles(**_kwargs) -> None:
|
||||
ensure_role_groups()
|
||||
assign_role_permissions()
|
||||
|
||||
post_migrate.connect(_ensure_roles, sender=self)
|
||||
post_migrate.connect(_ensure_roles, dispatch_uid="core_rbac")
|
||||
return super().ready()
|
||||
|
||||
51
app/apps/core/management/commands/sync_object_perms.py
Normal file
51
app/apps/core/management/commands/sync_object_perms.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from apps.access.models import AccessRequest
|
||||
from apps.core.rbac import assign_default_object_permissions
|
||||
from apps.keys.models import SSHKey
|
||||
from apps.servers.models import Server
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Backfill guardian object permissions for access requests and SSH keys."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
access_count = 0
|
||||
for access_request in AccessRequest.objects.select_related("requester"):
|
||||
if not access_request.requester_id:
|
||||
assign_default_object_permissions(access_request)
|
||||
else:
|
||||
for perm in (
|
||||
"access.view_accessrequest",
|
||||
"access.change_accessrequest",
|
||||
"access.delete_accessrequest",
|
||||
):
|
||||
assign_perm(perm, access_request.requester, access_request)
|
||||
assign_default_object_permissions(access_request)
|
||||
access_count += 1
|
||||
|
||||
key_count = 0
|
||||
for key in SSHKey.objects.select_related("user"):
|
||||
if not key.user_id:
|
||||
assign_default_object_permissions(key)
|
||||
else:
|
||||
for perm in ("keys.view_sshkey", "keys.change_sshkey", "keys.delete_sshkey"):
|
||||
assign_perm(perm, key.user, key)
|
||||
assign_default_object_permissions(key)
|
||||
key_count += 1
|
||||
|
||||
server_count = 0
|
||||
for server in Server.objects.all():
|
||||
assign_default_object_permissions(server)
|
||||
server_count += 1
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"Synced object permissions for "
|
||||
f"{access_count} access requests, "
|
||||
f"{key_count} SSH keys, "
|
||||
f"and {server_count} servers."
|
||||
)
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from guardian.shortcuts import assign_perm
|
||||
from ninja.errors import HttpError
|
||||
|
||||
ROLE_ADMIN = "administrator"
|
||||
@@ -13,6 +14,38 @@ ROLE_ALL = ROLE_ORDER
|
||||
ROLE_ALIASES = {"admin": ROLE_ADMIN}
|
||||
ROLE_INPUTS = tuple(sorted(set(ROLE_ORDER) | set(ROLE_ALIASES.keys())))
|
||||
|
||||
def _model_perms(app_label: str, model: str, actions: list[str]) -> list[str]:
|
||||
return [f"{app_label}.{action}_{model}" for action in actions]
|
||||
|
||||
|
||||
ROLE_PERMISSIONS = {
|
||||
ROLE_ADMIN: [],
|
||||
ROLE_OPERATOR: [
|
||||
*_model_perms("servers", "server", ["view"]),
|
||||
*_model_perms("access", "accessrequest", ["add", "view", "change", "delete"]),
|
||||
*_model_perms("keys", "sshkey", ["add", "view", "change", "delete"]),
|
||||
*_model_perms("telemetry", "telemetryevent", ["add", "view"]),
|
||||
*_model_perms("audit", "auditlog", ["view"]),
|
||||
*_model_perms("audit", "auditeventtype", ["view"]),
|
||||
*_model_perms("auth", "user", ["add", "view"]),
|
||||
],
|
||||
ROLE_AUDITOR: [
|
||||
*_model_perms("audit", "auditlog", ["view"]),
|
||||
*_model_perms("audit", "auditeventtype", ["view"]),
|
||||
],
|
||||
ROLE_USER: [
|
||||
*_model_perms("servers", "server", ["view"]),
|
||||
*_model_perms("access", "accessrequest", ["add"]),
|
||||
*_model_perms("keys", "sshkey", ["add"]),
|
||||
],
|
||||
}
|
||||
|
||||
OBJECT_PERMISSION_MODELS = {
|
||||
("servers", "server"),
|
||||
("access", "accessrequest"),
|
||||
("keys", "sshkey"),
|
||||
}
|
||||
|
||||
|
||||
def normalize_role(role: str) -> str:
|
||||
normalized = (role or "").strip().lower()
|
||||
@@ -24,6 +57,56 @@ def ensure_role_groups() -> None:
|
||||
Group.objects.get_or_create(name=role)
|
||||
|
||||
|
||||
def assign_role_permissions() -> None:
|
||||
ensure_role_groups()
|
||||
for role, perm_codes in ROLE_PERMISSIONS.items():
|
||||
group = Group.objects.get(name=role)
|
||||
if role == ROLE_ADMIN:
|
||||
group.permissions.set(Permission.objects.all())
|
||||
continue
|
||||
perms = []
|
||||
for code in perm_codes:
|
||||
if "." not in code:
|
||||
continue
|
||||
app_label, codename = code.split(".", 1)
|
||||
try:
|
||||
perms.append(
|
||||
Permission.objects.get(
|
||||
content_type__app_label=app_label,
|
||||
codename=codename,
|
||||
)
|
||||
)
|
||||
except Permission.DoesNotExist:
|
||||
continue
|
||||
group.permissions.set(perms)
|
||||
|
||||
|
||||
def assign_default_object_permissions(instance) -> None:
|
||||
app_label = instance._meta.app_label
|
||||
model_name = instance._meta.model_name
|
||||
if (app_label, model_name) not in OBJECT_PERMISSION_MODELS:
|
||||
return
|
||||
ensure_role_groups()
|
||||
groups = {group.name: group for group in Group.objects.filter(name__in=ROLE_ORDER)}
|
||||
for role, perm_codes in ROLE_PERMISSIONS.items():
|
||||
if role == ROLE_ADMIN:
|
||||
continue
|
||||
group = groups.get(role)
|
||||
if not group:
|
||||
continue
|
||||
for code in perm_codes:
|
||||
if "." not in code:
|
||||
continue
|
||||
perm_app, codename = code.split(".", 1)
|
||||
if perm_app != app_label:
|
||||
continue
|
||||
if not codename.endswith(f"_{model_name}"):
|
||||
continue
|
||||
if codename.startswith("add_"):
|
||||
continue
|
||||
assign_perm(code, group, instance)
|
||||
|
||||
|
||||
def get_user_role(user, default: str = ROLE_USER) -> str | None:
|
||||
if not user or not getattr(user, "is_authenticated", False):
|
||||
return None
|
||||
@@ -33,8 +116,6 @@ def get_user_role(user, default: str = ROLE_USER) -> str | None:
|
||||
for role in ROLE_ORDER:
|
||||
if role in group_names:
|
||||
return role
|
||||
if getattr(user, "is_staff", False):
|
||||
return ROLE_ADMIN
|
||||
return default
|
||||
|
||||
|
||||
@@ -51,6 +132,9 @@ def set_user_role(user, role: str) -> str:
|
||||
if canonical == ROLE_ADMIN:
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
elif canonical in {ROLE_OPERATOR, ROLE_AUDITOR}:
|
||||
user.is_staff = True
|
||||
user.is_superuser = False
|
||||
else:
|
||||
user.is_staff = False
|
||||
user.is_superuser = False
|
||||
@@ -63,13 +147,9 @@ def require_authenticated(request) -> None:
|
||||
raise HttpError(403, "Forbidden")
|
||||
|
||||
|
||||
def require_roles(request, *roles: str) -> None:
|
||||
def require_perms(request, *perms: str) -> None:
|
||||
user = getattr(request, "user", None)
|
||||
if not user or not getattr(user, "is_authenticated", False):
|
||||
raise HttpError(403, "Forbidden")
|
||||
role = get_user_role(user)
|
||||
if role == ROLE_ADMIN:
|
||||
return
|
||||
allowed = {normalize_role(entry) for entry in roles}
|
||||
if role not in allowed:
|
||||
if not user.has_perms(perms):
|
||||
raise HttpError(403, "Forbidden")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
|
||||
from .models import SSHKey
|
||||
|
||||
|
||||
@admin.register(SSHKey)
|
||||
class SSHKeyAdmin(admin.ModelAdmin):
|
||||
class SSHKeyAdmin(GuardedModelAdmin):
|
||||
list_display = ("id", "user", "name", "key_type", "fingerprint", "is_active", "created_at")
|
||||
list_filter = ("is_active", "key_type")
|
||||
search_fields = ("name", "user__username", "user__email", "fingerprint")
|
||||
|
||||
@@ -5,3 +5,7 @@ class KeysConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.keys"
|
||||
verbose_name = "SSH Keys"
|
||||
|
||||
def ready(self) -> None:
|
||||
from . import signals # noqa: F401
|
||||
return super().ready()
|
||||
|
||||
19
app/apps/keys/signals.py
Normal file
19
app/apps/keys/signals.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from apps.core.rbac import assign_default_object_permissions
|
||||
from .models import SSHKey
|
||||
|
||||
|
||||
@receiver(post_save, sender=SSHKey)
|
||||
def assign_ssh_key_perms(sender, instance: SSHKey, created: bool, **kwargs) -> None:
|
||||
if not created:
|
||||
return
|
||||
if instance.user_id:
|
||||
user = instance.user
|
||||
for perm in ("keys.view_sshkey", "keys.change_sshkey", "keys.delete_sshkey"):
|
||||
assign_perm(perm, user, instance)
|
||||
assign_default_object_permissions(instance)
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
from django.utils.html import format_html
|
||||
from .models import Server
|
||||
|
||||
|
||||
@admin.register(Server)
|
||||
class ServerAdmin(admin.ModelAdmin):
|
||||
class ServerAdmin(GuardedModelAdmin):
|
||||
list_display = ("avatar", "display_name", "hostname", "ipv4", "ipv6", "created_at")
|
||||
list_display_links = ("display_name",)
|
||||
search_fields = ("display_name", "hostname", "ipv4", "ipv6")
|
||||
@@ -26,4 +27,3 @@ class ServerAdmin(admin.ModelAdmin):
|
||||
)
|
||||
avatar.short_description = ""
|
||||
|
||||
|
||||
|
||||
@@ -6,4 +6,7 @@ class ServersConfig(AppConfig):
|
||||
name = "apps.servers"
|
||||
verbose_name = "Servers"
|
||||
|
||||
def ready(self) -> None:
|
||||
from . import signals # noqa: F401
|
||||
return super().ready()
|
||||
|
||||
|
||||
14
app/apps/servers/signals.py
Normal file
14
app/apps/servers/signals.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from apps.core.rbac import assign_default_object_permissions
|
||||
from .models import Server
|
||||
|
||||
|
||||
@receiver(post_save, sender=Server)
|
||||
def assign_server_perms(sender, instance: Server, created: bool, **kwargs) -> None:
|
||||
if not created:
|
||||
return
|
||||
assign_default_object_permissions(instance)
|
||||
Reference in New Issue
Block a user