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:
parent
142b26d7da
commit
46e910ba25
3 changed files with 679 additions and 0 deletions
185
migrations.py
185
migrations.py
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue