Add RBAC (Role-Based Access Control) system - Phase 1

Implemented comprehensive role-based permission management system:

Database:
- Added m004_add_rbac_tables migration
- roles table: Define named permission bundles (Employee, Contractor, etc.)
- role_permissions table: Map roles to account permissions
- user_roles table: Assign users to roles with optional expiration
- Created 4 default roles: Employee (default), Contractor, Accountant, Manager

Models (models.py):
- Role, CreateRole, UpdateRole
- RolePermission, CreateRolePermission
- UserRole, AssignUserRole
- RoleWithPermissions, UserWithRoles

CRUD Operations (crud.py):
- Role management: create_role, get_role, get_all_roles, update_role, delete_role
- get_default_role() - get auto-assigned role for new users
- Role permissions: create_role_permission, get_role_permissions, delete_role_permission
- User role assignment: assign_user_role, get_user_roles, revoke_user_role
- Helper functions:
  - get_user_permissions_from_roles() - resolve user permissions via roles
  - check_user_has_role_permission() - check role-based access
  - auto_assign_default_role() - auto-assign default role to new users

Permission Resolution Order:
1. Individual account_permissions (direct grants/exceptions)
2. Role-based permissions (via user_roles → role_permissions)
3. Inherited permissions (hierarchical account names)
4. Deny by default

Next: API endpoints, UI, and permission resolution logic integration

🤖 Generated with Claude Code
This commit is contained in:
padreug 2025-11-11 23:34:28 +01:00
parent 142b26d7da
commit 46e910ba25
3 changed files with 679 additions and 0 deletions

View file

@ -410,3 +410,188 @@ async def m003_add_account_is_virtual(db):
"description": description,
},
)
async def m004_add_rbac_tables(db):
"""
Add Role-Based Access Control (RBAC) tables.
This migration introduces a flexible RBAC system that complements
the existing individual permission grants:
- Roles: Named bundles of permissions (Employee, Contractor, Admin, etc.)
- Role Permissions: Define what accounts each role can access
- User Roles: Assign users to roles
- Default Role: Auto-assign new users to a default role
Permission Resolution Order:
1. Individual account_permissions (exceptions/overrides)
2. Role-based permissions via user_roles
3. Inherited permissions (hierarchical account names)
4. Deny by default
"""
# =========================================================================
# ROLES TABLE
# =========================================================================
# Define named roles (Employee, Contractor, Admin, etc.)
await db.execute(
f"""
CREATE TABLE roles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_by TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
"""
CREATE INDEX idx_roles_name ON roles (name);
"""
)
await db.execute(
"""
CREATE INDEX idx_roles_is_default ON roles (is_default)
WHERE is_default = TRUE;
"""
)
# =========================================================================
# ROLE PERMISSIONS TABLE
# =========================================================================
# Define which accounts each role can access and with what permission type
await db.execute(
f"""
CREATE TABLE role_permissions (
id TEXT PRIMARY KEY,
role_id TEXT NOT NULL,
account_id TEXT NOT NULL,
permission_type TEXT NOT NULL,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
);
"""
)
await db.execute(
"""
CREATE INDEX idx_role_permissions_role_id ON role_permissions (role_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_role_permissions_account_id ON role_permissions (account_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_role_permissions_type ON role_permissions (permission_type);
"""
)
# =========================================================================
# USER ROLES TABLE
# =========================================================================
# Assign users to roles
await db.execute(
f"""
CREATE TABLE user_roles (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
expires_at TIMESTAMP,
notes TEXT,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE
);
"""
)
await db.execute(
"""
CREATE INDEX idx_user_roles_user_id ON user_roles (user_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_user_roles_role_id ON user_roles (role_id);
"""
)
await db.execute(
"""
CREATE INDEX idx_user_roles_expires ON user_roles (expires_at)
WHERE expires_at IS NOT NULL;
"""
)
# Composite index for checking specific user+role assignments
await db.execute(
"""
CREATE INDEX idx_user_roles_user_role ON user_roles (user_id, role_id);
"""
)
# =========================================================================
# CREATE DEFAULT ROLES
# =========================================================================
# Insert standard roles that most organizations will use
import uuid
# Define default roles and their descriptions
default_roles = [
(
"employee",
"Employee",
"Standard employee role with access to common expense accounts",
True, # This is the default role for new users
),
(
"contractor",
"Contractor",
"External contractor with limited expense account access",
False,
),
(
"accountant",
"Accountant",
"Accounting staff with read access to financial accounts",
False,
),
(
"manager",
"Manager",
"Management role with broader expense approval and account access",
False,
),
]
for slug, name, description, is_default in default_roles:
await db.execute(
f"""
INSERT INTO roles (id, name, description, is_default, created_by, created_at)
VALUES (:id, :name, :description, :is_default, :created_by, {db.timestamp_now})
""",
{
"id": str(uuid.uuid4()),
"name": name,
"description": description,
"is_default": is_default,
"created_by": "system", # System-created default roles
},
)