fix error in gitignore
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# rbac app
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RbacConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.rbac"
|
||||
@@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.2.17 on 2026-09-15 09:29
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Permission',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('code', models.CharField(max_length=120, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=255)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=120)),
|
||||
('slug', models.SlugField(max_length=120, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=255)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RolePermission',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.permission')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.role')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='permissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='roles', through='rbac.RolePermission', to='rbac.permission'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserRole',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('scope', models.JSONField(blank=True, default=dict)),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_roles', to='rbac.role')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_roles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rolepermission',
|
||||
constraint=models.UniqueConstraint(fields=('role', 'permission'), name='uniq_role_permission'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userrole',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'role'), name='uniq_user_role'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
from django.db import models
|
||||
|
||||
from apps.core.models import TimeStampedModel, UUIDModel
|
||||
|
||||
|
||||
class Permission(UUIDModel):
|
||||
code = models.CharField(max_length=120, unique=True)
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.code
|
||||
|
||||
|
||||
class Role(UUIDModel, TimeStampedModel):
|
||||
name = models.CharField(max_length=120)
|
||||
slug = models.SlugField(max_length=120, unique=True)
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
permissions = models.ManyToManyField(Permission, through="RolePermission", related_name="roles", blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class RolePermission(UUIDModel):
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["role", "permission"], name="uniq_role_permission")
|
||||
]
|
||||
|
||||
|
||||
class UserRole(UUIDModel, TimeStampedModel):
|
||||
user = models.ForeignKey("accounts.User", on_delete=models.CASCADE, related_name="user_roles")
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="user_roles")
|
||||
scope = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["user", "role"], name="uniq_user_role")
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
"""RBAC helpers and DRF permission classes.
|
||||
|
||||
Authorization is always enforced on the backend. The frontend may hide UI
|
||||
elements, but that is never treated as a security boundary.
|
||||
"""
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from apps.rbac.models import Permission
|
||||
|
||||
|
||||
def has_perm(user, code: str) -> bool:
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
if user.is_superuser:
|
||||
return True
|
||||
return Permission.objects.filter(roles__user_roles__user=user, code=code).exists()
|
||||
|
||||
|
||||
def has_any_perm(user, codes) -> bool:
|
||||
return any(has_perm(user, c) for c in codes)
|
||||
|
||||
|
||||
def user_role_slugs(user):
|
||||
if not user or not user.is_authenticated:
|
||||
return []
|
||||
return list(user.user_roles.values_list("role__slug", flat=True))
|
||||
|
||||
|
||||
class HasPermission(BasePermission):
|
||||
"""Global permission check. Set `perm_code` (or `perm_codes`) on the view."""
|
||||
|
||||
perm_code = None
|
||||
perm_codes = None
|
||||
|
||||
def has_permission(self, request, view):
|
||||
codes = self.perm_codes or ([self.perm_code] if self.perm_code else None)
|
||||
if not codes:
|
||||
return True
|
||||
return has_any_perm(request.user, codes)
|
||||
|
||||
|
||||
class ActionPermission(BasePermission):
|
||||
"""Maps a DRF viewset action name to a permission code."""
|
||||
|
||||
code_map = {}
|
||||
|
||||
def has_permission(self, request, view):
|
||||
code = self.code_map.get(getattr(view, "action", None))
|
||||
if code is None:
|
||||
return True
|
||||
return has_perm(request.user, code)
|
||||
|
||||
|
||||
class IsOwner(BasePermission):
|
||||
"""Object-level ownership for customers accessing their own resources."""
|
||||
|
||||
owner_field = "user"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return getattr(obj, self.owner_field, None) == request.user
|
||||
@@ -0,0 +1,17 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.rbac.models import Permission, Role
|
||||
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ["id", "code", "description"]
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
permissions = PermissionSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ["id", "name", "slug", "description", "permissions"]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.rbac.models import Permission, Role
|
||||
from apps.rbac.permissions import has_perm
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class RbacTests(TestCase):
|
||||
def test_has_perm_grants_and_denies(self):
|
||||
user = User.objects.create_user(username="op", password="x")
|
||||
perm = Permission.objects.create(code="processing.start")
|
||||
role = Role.objects.create(name="Operator", slug="operator")
|
||||
role.permissions.add(perm)
|
||||
user.user_roles.create(role=role)
|
||||
|
||||
self.assertTrue(has_perm(user, "processing.start"))
|
||||
self.assertFalse(has_perm(user, "workflow.publish"))
|
||||
|
||||
def test_superuser_has_all(self):
|
||||
user = User.objects.create_superuser(username="admin", password="x")
|
||||
self.assertTrue(has_perm(user, "anything.at.all"))
|
||||
@@ -0,0 +1,19 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from apps.rbac.models import Role
|
||||
from apps.rbac.permissions import ActionPermission
|
||||
from apps.rbac.serializers import RoleSerializer
|
||||
|
||||
|
||||
class RoleViewSet(viewsets.ModelViewSet):
|
||||
queryset = Role.objects.prefetch_related("permissions").all()
|
||||
serializer_class = RoleSerializer
|
||||
permission_classes = [ActionPermission]
|
||||
code_map = {
|
||||
"list": "role.view",
|
||||
"retrieve": "role.view",
|
||||
"create": "role.manage",
|
||||
"update": "role.manage",
|
||||
"partial_update": "role.manage",
|
||||
"destroy": "role.manage",
|
||||
}
|
||||
Reference in New Issue
Block a user