Files
2026-06-26 11:41:00 +05:30

294 lines
9.5 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
from app.core.http_responses import not_found_response
from sqlalchemy import select
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.audit.service import write_audit_log
from app.modules.core.iam.scope import build_scope, assert_can_manage_role_object
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.models import Permission, Role, RolePermission
from app.modules.core.rbac.services import build_permissions_payload, build_roles_payload
router = APIRouter(prefix="/system-settings/rbac", tags=["rbac-ui"])
def _csrf_rejected(request: Request):
from app.core.http_responses import forbidden_response
return forbidden_response(request, "CSRF validation failed")
def _redirect_login():
return RedirectResponse(url="/login", status_code=303)
def _redirect_denied():
from app.core.http_responses import ui_access_denied
return ui_access_denied()
def _is_system_admin(db, current_user) -> bool:
return "System Admin" in get_user_roles(db, current_user.id)
def _base_ctx(request: Request, current_user, db, **ctx):
base = {
"request": request,
"current_user": current_user,
"current_user_roles": get_user_roles(db, current_user.id),
"current_user_permissions": get_user_permissions(db, current_user.id),
"csrf_token": get_or_create_csrf_token(request),
}
base.update(ctx)
return base
@router.get("")
def rbac_dashboard(request: Request):
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
# Phase 3 security hardening: the roles list endpoint must not accept direct unsafe POSTs.
# Existing role creation continues to use /roles/new.
@router.post("/roles")
def roles_list_post_rejected(request: Request):
return _csrf_rejected(request)
@router.get("/roles")
def roles_list(request: Request, q: str = "", page: int = 1, per_page: int = 10):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
scope = build_scope(db, current_user)
payload = build_roles_payload(db, scope, q=q, page=page, per_page=per_page)
return templates.TemplateResponse(
"modules/core/rbac/templates/roles_list.html",
_base_ctx(request, current_user, db, title="RBAC Roles", **payload),
)
finally:
db.close()
@router.post("/roles/new")
def role_create_submit(
request: Request,
name: str = Form(...),
is_active: str | None = Form(None),
csrf_token: str = Form(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
name = name.strip()
if name and not db.execute(select(Role).where(Role.name == name)).scalar_one_or_none():
role = Role(name=name, is_active=is_active is not None)
db.add(role)
db.commit()
db.refresh(role)
write_audit_log(
db,
action="role.create",
entity_type="role",
actor=current_user,
request=request,
entity_id=role.id,
entity_name=role.name,
target_tenant_id=current_user.tenant_id,
target_branch_id=current_user.branch_id,
details={"name": role.name, "is_active": role.is_active},
)
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
finally:
db.close()
@router.get("/roles/{role_id}")
def role_permissions_edit(request: Request, role_id: int):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
role = db.execute(select(Role).where(Role.id == role_id)).scalar_one_or_none()
if not role:
return not_found_response(request, "Role not found")
scope = build_scope(db, current_user)
try:
assert_can_manage_role_object(scope, role)
except Exception:
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
permissions = db.execute(select(Permission).order_by(Permission.code)).scalars().all()
assigned_permission_ids = db.execute(
select(RolePermission.permission_id).where(RolePermission.role_id == role_id)
).scalars().all()
return templates.TemplateResponse(
"modules/core/rbac/templates/role_permissions_form.html",
_base_ctx(
request,
current_user,
db,
title=f"Role Permissions - {role.name}",
role=role,
permissions=permissions,
assigned_permission_ids=list(assigned_permission_ids),
),
)
finally:
db.close()
@router.post("/roles/{role_id}")
def role_permissions_submit(
request: Request,
role_id: int,
permission_ids: list[int] = Form([]),
csrf_token: str = Form(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
role = db.execute(select(Role).where(Role.id == role_id)).scalar_one_or_none()
if not role:
return not_found_response(request, "Role not found")
old_permission_ids = db.execute(
select(RolePermission.permission_id).where(RolePermission.role_id == role.id)
).scalars().all()
db.execute(RolePermission.__table__.delete().where(RolePermission.role_id == role.id))
for permission_id in permission_ids:
db.add(RolePermission(role_id=role.id, permission_id=permission_id))
db.commit()
write_audit_log(
db,
action="role.permissions.update",
entity_type="role",
actor=current_user,
request=request,
entity_id=role.id,
entity_name=role.name,
target_tenant_id=current_user.tenant_id,
target_branch_id=current_user.branch_id,
details={
"old_permission_ids": list(old_permission_ids),
"new_permission_ids": list(permission_ids),
},
)
return RedirectResponse(url="/system-settings/rbac/roles", status_code=303)
finally:
db.close()
@router.get("/permissions")
def permissions_list(request: Request, q: str = "", page: int = 1, per_page: int = 15):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
payload = build_permissions_payload(db, q=q, page=page, per_page=per_page)
return templates.TemplateResponse(
"modules/core/rbac/templates/permissions_list.html",
_base_ctx(request, current_user, db, title="RBAC Permissions", **payload),
)
finally:
db.close()
@router.post("/permissions/new")
def permission_create_submit(
request: Request,
code: str = Form(...),
name: str = Form(...),
is_active: str | None = Form(None),
csrf_token: str = Form(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_system_admin(db, current_user):
return _redirect_denied()
code = code.strip()
if code and not db.execute(select(Permission).where(Permission.code == code)).scalar_one_or_none():
permission = Permission(code=code, name=name.strip(), is_active=is_active is not None)
db.add(permission)
db.commit()
db.refresh(permission)
write_audit_log(
db,
action="permission.create",
entity_type="permission",
actor=current_user,
request=request,
entity_id=permission.id,
entity_name=permission.code,
target_tenant_id=current_user.tenant_id,
target_branch_id=current_user.branch_id,
details={"code": permission.code, "name": permission.name, "is_active": permission.is_active},
)
return RedirectResponse(url="/system-settings/rbac/permissions", status_code=303)
finally:
db.close()