294 lines
9.5 KiB
Python
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()
|