feat: granular RBAC system with user groups, deny-list, permission registry, system-admin, self-mod prevention

This commit is contained in:
Agent Zero
2026-07-15 21:59:45 +02:00
parent 905bc9b744
commit b490a62322
17 changed files with 1408 additions and 13 deletions
+3
View File
@@ -8,6 +8,7 @@ from app.models.auth import ApiToken, PasswordResetToken
from app.models.company import Company
from app.models.contact import CompanyContact, Contact
from app.models.currency import Currency
from app.models.group import Group, UserGroup
from app.models.notification import Notification, NotificationPreference, NotificationType
from app.models.plugin import Plugin, PluginMigration
from app.models.role import Role
@@ -24,6 +25,8 @@ __all__ = [
"User",
"UserTenant",
"Role",
"Group",
"UserGroup",
"Session",
"AuditLog",
"DeletionLog",
+63
View File
@@ -0,0 +1,63 @@
"""Group and UserGroup models for granular RBAC."""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class Group(Base, TenantMixin):
"""Group entity with RBAC permissions and field-level permissions.
Groups are tenant-scoped. Users can be members of multiple groups.
Effective permissions = (role.permissions Σ(group.permissions)) denied.
"""
__tablename__ = "groups"
__table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_groups_tenant_name"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str] = mapped_column(String(500), nullable=True, default=None)
permissions: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
denied_permissions: Mapped[list[str]] = mapped_column(
JSONB, nullable=False, default=list
)
field_permissions: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
permission_version: Mapped[int] = mapped_column(
Integer, nullable=False, default=1, server_default="1"
)
class UserGroup(Base):
"""N:M association — user membership in groups (per tenant)."""
__tablename__ = "user_groups"
__table_args__ = (
UniqueConstraint("user_id", "group_id", "tenant_id", name="uq_user_groups_user_group_tenant"),
)
user_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
)
group_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True
)
tenant_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+7 -1
View File
@@ -6,7 +6,7 @@ import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, String, func
from sqlalchemy import DateTime, Integer, String, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
@@ -24,7 +24,13 @@ class Role(Base, TenantMixin):
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
permissions: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
denied_permissions: Mapped[list[str]] = mapped_column(
JSONB, nullable=False, default=list
)
field_permissions: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
permission_version: Mapped[int] = mapped_column(
Integer, nullable=False, default=1, server_default="1"
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+9
View File
@@ -35,6 +35,9 @@ class User(Base, TenantMixin):
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
preferences: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False)
is_system_admin: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False, server_default="false"
)
class UserTenant(Base):
@@ -49,6 +52,12 @@ class UserTenant(Base):
PGUUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), primary_key=True
)
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
role_id: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("roles.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)