fix error in gitignore

This commit is contained in:
2026-09-24 12:32:59 +03:30
commit 86c117ee2a
165 changed files with 5963 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# rbac app
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class RbacConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.rbac"
+75
View File
@@ -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'),
),
]
View File
+42
View File
@@ -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")
]
+60
View File
@@ -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
+17
View File
@@ -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"]
+23
View File
@@ -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"))
+19
View File
@@ -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",
}