feat: implement complete RBAC access control with tests
Some checks failed
CI / pytest (push) Failing after 45m29s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 28s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 9s

Add 4-layer access control stack (subscription → module → menu → permissions):
- P1: Wire requires_permission into menu sidebar filtering
- P2: Expose window.USER_PERMISSIONS for Alpine.js client-side gating
- P3: Add page-level permission guards on store routes
- P4: Role CRUD API endpoints and role editor UI
- P5: Audit trail for all role/permission changes

Includes unit tests (menu permission filtering, role CRUD service) and
integration tests (role API endpoints). All 404 core+tenancy tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 18:26:59 +01:00
parent 962862ccc1
commit cb3bc3c118
29 changed files with 1850 additions and 17 deletions

View File

@@ -1557,6 +1557,55 @@ def get_user_permissions(
return []
# ============================================================================
# PAGE-LEVEL PERMISSION GUARDS (For Store Page Routes)
# ============================================================================
def require_store_page_permission(permission: str):
"""
Dependency factory to require a specific store permission for page routes.
Same as require_store_permission but raises InsufficientStorePermissionsException
which the exception handler intercepts for HTML requests (redirecting to login).
Usage:
@router.get("/products", response_class=HTMLResponse)
def store_products_page(
request: Request,
store_code: str = Depends(get_resolved_store_code),
current_user: User = Depends(require_store_page_permission("products.view")),
db: Session = Depends(get_db),
):
...
"""
def permission_checker(
request: Request,
db: Session = Depends(get_db),
current_user: UserContext = Depends(get_current_store_from_cookie_or_header),
) -> UserContext:
if not current_user.token_store_id:
raise InvalidTokenException(
"Token missing store information. Please login again."
)
store_id = current_user.token_store_id
store = store_service.get_store_by_id(db, store_id)
request.state.store = store
user_model = _get_user_model(current_user, db)
if not user_model.has_store_permission(store.id, permission):
raise InsufficientStorePermissionsException(
required_permission=permission,
store_code=store.store_code,
)
return current_user
return permission_checker
# ============================================================================
# OPTIONAL AUTHENTICATION (For Login Page Redirects)
# ============================================================================