Skip to content

Commit 545fb71

Browse files
committed
implement RBAC
1 parent f258166 commit 545fb71

File tree

27 files changed

+3024
-158
lines changed

27 files changed

+3024
-158
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""add_rbac_tables
2+
3+
Revision ID: ffdac40437ff
4+
Revises: 832f5763b245
5+
Create Date: 2025-12-14 14:52:10.919953
6+
7+
"""
8+
import uuid
9+
from datetime import datetime, timezone
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
import sqlmodel.sql.sqltypes
14+
15+
16+
# revision identifiers, used by Alembic.
17+
revision = 'ffdac40437ff'
18+
down_revision = '832f5763b245'
19+
branch_labels = None
20+
depends_on = None
21+
22+
23+
def upgrade():
24+
# ### commands auto generated by Alembic - please adjust! ###
25+
op.create_table('permission',
26+
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
27+
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
28+
sa.Column('resource', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False),
29+
sa.Column('action', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False),
30+
sa.Column('id', sa.Uuid(), nullable=False),
31+
sa.Column('created_at', sa.DateTime(), nullable=False),
32+
sa.PrimaryKeyConstraint('id')
33+
)
34+
op.create_index(op.f('ix_permission_name'), 'permission', ['name'], unique=True)
35+
op.create_table('role',
36+
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
37+
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
38+
sa.Column('is_system', sa.Boolean(), nullable=False),
39+
sa.Column('id', sa.Uuid(), nullable=False),
40+
sa.Column('created_at', sa.DateTime(), nullable=False),
41+
sa.PrimaryKeyConstraint('id')
42+
)
43+
op.create_index(op.f('ix_role_name'), 'role', ['name'], unique=True)
44+
op.create_table('role_permission',
45+
sa.Column('role_id', sa.Uuid(), nullable=False),
46+
sa.Column('permission_id', sa.Uuid(), nullable=False),
47+
sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ),
48+
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
49+
sa.PrimaryKeyConstraint('role_id', 'permission_id')
50+
)
51+
op.create_table('user_role',
52+
sa.Column('user_id', sa.Uuid(), nullable=False),
53+
sa.Column('role_id', sa.Uuid(), nullable=False),
54+
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
55+
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
56+
sa.PrimaryKeyConstraint('user_id', 'role_id')
57+
)
58+
# ### end Alembic commands ###
59+
60+
# Seed default permissions
61+
permission_table = sa.table(
62+
'permission',
63+
sa.column('id', sa.Uuid()),
64+
sa.column('name', sa.String()),
65+
sa.column('description', sa.String()),
66+
sa.column('resource', sa.String()),
67+
sa.column('action', sa.String()),
68+
sa.column('created_at', sa.DateTime()),
69+
)
70+
71+
now = datetime.now(timezone.utc)
72+
73+
# Define all permissions
74+
permissions = [
75+
# Users permissions
76+
{'id': uuid.uuid4(), 'name': 'users:read', 'description': 'View users', 'resource': 'users', 'action': 'read', 'created_at': now},
77+
{'id': uuid.uuid4(), 'name': 'users:create', 'description': 'Create users', 'resource': 'users', 'action': 'create', 'created_at': now},
78+
{'id': uuid.uuid4(), 'name': 'users:update', 'description': 'Update users', 'resource': 'users', 'action': 'update', 'created_at': now},
79+
{'id': uuid.uuid4(), 'name': 'users:delete', 'description': 'Delete users', 'resource': 'users', 'action': 'delete', 'created_at': now},
80+
# Items permissions
81+
{'id': uuid.uuid4(), 'name': 'items:read', 'description': 'View items', 'resource': 'items', 'action': 'read', 'created_at': now},
82+
{'id': uuid.uuid4(), 'name': 'items:create', 'description': 'Create items', 'resource': 'items', 'action': 'create', 'created_at': now},
83+
{'id': uuid.uuid4(), 'name': 'items:update', 'description': 'Update items', 'resource': 'items', 'action': 'update', 'created_at': now},
84+
{'id': uuid.uuid4(), 'name': 'items:delete', 'description': 'Delete items', 'resource': 'items', 'action': 'delete', 'created_at': now},
85+
# Roles permissions
86+
{'id': uuid.uuid4(), 'name': 'roles:read', 'description': 'View roles', 'resource': 'roles', 'action': 'read', 'created_at': now},
87+
{'id': uuid.uuid4(), 'name': 'roles:create', 'description': 'Create roles', 'resource': 'roles', 'action': 'create', 'created_at': now},
88+
{'id': uuid.uuid4(), 'name': 'roles:update', 'description': 'Update roles', 'resource': 'roles', 'action': 'update', 'created_at': now},
89+
{'id': uuid.uuid4(), 'name': 'roles:delete', 'description': 'Delete roles', 'resource': 'roles', 'action': 'delete', 'created_at': now},
90+
# Permissions (read-only)
91+
{'id': uuid.uuid4(), 'name': 'permissions:read', 'description': 'View permissions', 'resource': 'permissions', 'action': 'read', 'created_at': now},
92+
]
93+
94+
op.bulk_insert(permission_table, permissions)
95+
96+
# Seed default roles
97+
role_table = sa.table(
98+
'role',
99+
sa.column('id', sa.Uuid()),
100+
sa.column('name', sa.String()),
101+
sa.column('description', sa.String()),
102+
sa.column('is_system', sa.Boolean()),
103+
sa.column('created_at', sa.DateTime()),
104+
)
105+
106+
admin_role_id = uuid.uuid4()
107+
editor_role_id = uuid.uuid4()
108+
viewer_role_id = uuid.uuid4()
109+
110+
roles = [
111+
{'id': admin_role_id, 'name': 'Admin', 'description': 'Full system access', 'is_system': True, 'created_at': now},
112+
{'id': editor_role_id, 'name': 'Editor', 'description': 'Can read and modify content', 'is_system': True, 'created_at': now},
113+
{'id': viewer_role_id, 'name': 'Viewer', 'description': 'Read-only access', 'is_system': True, 'created_at': now},
114+
]
115+
116+
op.bulk_insert(role_table, roles)
117+
118+
# Seed role-permission associations
119+
role_permission_table = sa.table(
120+
'role_permission',
121+
sa.column('role_id', sa.Uuid()),
122+
sa.column('permission_id', sa.Uuid()),
123+
)
124+
125+
# Get permission IDs by name for role assignments
126+
perm_ids = {p['name']: p['id'] for p in permissions}
127+
128+
role_permissions = []
129+
130+
# Admin gets all permissions
131+
for perm in permissions:
132+
role_permissions.append({'role_id': admin_role_id, 'permission_id': perm['id']})
133+
134+
# Editor gets read/create/update on users and items
135+
editor_perms = ['users:read', 'users:create', 'users:update', 'items:read', 'items:create', 'items:update', 'roles:read', 'permissions:read']
136+
for perm_name in editor_perms:
137+
role_permissions.append({'role_id': editor_role_id, 'permission_id': perm_ids[perm_name]})
138+
139+
# Viewer gets read-only
140+
viewer_perms = ['users:read', 'items:read', 'roles:read', 'permissions:read']
141+
for perm_name in viewer_perms:
142+
role_permissions.append({'role_id': viewer_role_id, 'permission_id': perm_ids[perm_name]})
143+
144+
op.bulk_insert(role_permission_table, role_permissions)
145+
146+
147+
def downgrade():
148+
# ### commands auto generated by Alembic - please adjust! ###
149+
op.drop_table('user_role')
150+
op.drop_table('role_permission')
151+
op.drop_index(op.f('ix_role_name'), table_name='role')
152+
op.drop_table('role')
153+
op.drop_index(op.f('ix_permission_name'), table_name='permission')
154+
op.drop_table('permission')
155+
# ### end Alembic commands ###

backend/app/api/router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from app.modules.items.routes import router as items_router
66
from app.modules.owasp_demo.routes import router as owasp_demo_router
77
from app.modules.private.routes import router as private_router
8+
from app.modules.rbac.routes import router as rbac_router
89
from app.modules.shortener.routes import router as shortener_router
910
from app.modules.users.routes import router as users_router
1011
from app.modules.utils.routes import router as utils_router
@@ -17,6 +18,7 @@
1718
api_router.include_router(utils_router)
1819
api_router.include_router(items_router)
1920
api_router.include_router(shortener_router)
21+
api_router.include_router(rbac_router, prefix="/rbac", tags=["rbac"])
2022

2123
# Include private routes only in local environment
2224
if settings.ENVIRONMENT == "local":
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""RBAC module exports."""
2+
3+
from app.modules.rbac.models import (
4+
Permission,
5+
PermissionBase,
6+
Role,
7+
RoleBase,
8+
RolePermissionLink,
9+
UserRoleLink,
10+
)
11+
from app.modules.rbac.routes import router
12+
13+
__all__ = [
14+
"Permission",
15+
"PermissionBase",
16+
"Role",
17+
"RoleBase",
18+
"RolePermissionLink",
19+
"UserRoleLink",
20+
"router",
21+
]

backend/app/modules/rbac/models.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""RBAC models for Role-Based Access Control."""
2+
3+
import uuid
4+
from datetime import datetime, timezone
5+
from typing import TYPE_CHECKING
6+
7+
from sqlmodel import Field, Relationship, SQLModel
8+
9+
if TYPE_CHECKING:
10+
from app.modules.users.models import User
11+
12+
13+
class RolePermissionLink(SQLModel, table=True):
14+
"""Junction table for Role-Permission many-to-many relationship."""
15+
16+
__tablename__ = "role_permission"
17+
18+
role_id: uuid.UUID = Field(foreign_key="role.id", primary_key=True)
19+
permission_id: uuid.UUID = Field(foreign_key="permission.id", primary_key=True)
20+
21+
22+
class UserRoleLink(SQLModel, table=True):
23+
"""Junction table for User-Role many-to-many relationship."""
24+
25+
__tablename__ = "user_role"
26+
27+
user_id: uuid.UUID = Field(foreign_key="user.id", primary_key=True)
28+
role_id: uuid.UUID = Field(foreign_key="role.id", primary_key=True)
29+
30+
31+
class PermissionBase(SQLModel):
32+
"""Base permission properties."""
33+
34+
name: str = Field(unique=True, index=True, max_length=100)
35+
description: str | None = Field(default=None, max_length=255)
36+
resource: str = Field(max_length=50) # e.g., "users", "items", "roles"
37+
action: str = Field(max_length=50) # e.g., "read", "create", "update", "delete"
38+
39+
40+
class Permission(PermissionBase, table=True):
41+
"""Permission database model."""
42+
43+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
44+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
45+
46+
# Relationships
47+
roles: list["Role"] = Relationship(
48+
back_populates="permissions", link_model=RolePermissionLink
49+
)
50+
51+
52+
class RoleBase(SQLModel):
53+
"""Base role properties."""
54+
55+
name: str = Field(unique=True, index=True, max_length=100)
56+
description: str | None = Field(default=None, max_length=255)
57+
is_system: bool = Field(default=False) # Protects default roles from deletion
58+
59+
60+
class Role(RoleBase, table=True):
61+
"""Role database model."""
62+
63+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
64+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
65+
66+
# Relationships
67+
permissions: list[Permission] = Relationship(
68+
back_populates="roles", link_model=RolePermissionLink
69+
)
70+
users: list["User"] = Relationship(
71+
back_populates="roles", link_model=UserRoleLink
72+
)

0 commit comments

Comments
 (0)