feat: implement complete RBAC access control with tests
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:
@@ -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)
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user