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:
@@ -28,7 +28,10 @@ from app.modules.tenancy.schemas.team import (
|
||||
InvitationAccept,
|
||||
InvitationAcceptResponse,
|
||||
InvitationResponse,
|
||||
RoleCreate,
|
||||
RoleListResponse,
|
||||
RoleResponse,
|
||||
RoleUpdate,
|
||||
TeamMemberInvite,
|
||||
TeamMemberListResponse,
|
||||
TeamMemberResponse,
|
||||
@@ -392,6 +395,86 @@ def list_roles(
|
||||
return RoleListResponse(roles=roles, total=len(roles))
|
||||
|
||||
|
||||
@store_team_router.post("/roles", response_model=RoleResponse, status_code=201)
|
||||
def create_role(
|
||||
request: Request,
|
||||
role_data: RoleCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(require_store_owner),
|
||||
):
|
||||
"""
|
||||
Create a custom role for the store.
|
||||
|
||||
**Required:** Store owner only.
|
||||
|
||||
Preset role names (manager, staff, support, viewer, marketing) cannot be used.
|
||||
"""
|
||||
store = request.state.store
|
||||
|
||||
role = store_team_service.create_custom_role(
|
||||
db=db,
|
||||
store_id=store.id,
|
||||
name=role_data.name,
|
||||
permissions=role_data.permissions,
|
||||
actor_user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
return role
|
||||
|
||||
|
||||
@store_team_router.put("/roles/{role_id}", response_model=RoleResponse)
|
||||
def update_role(
|
||||
request: Request,
|
||||
role_id: int,
|
||||
role_data: RoleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(require_store_owner),
|
||||
):
|
||||
"""
|
||||
Update a role's name and/or permissions.
|
||||
|
||||
**Required:** Store owner only.
|
||||
"""
|
||||
store = request.state.store
|
||||
|
||||
role = store_team_service.update_role(
|
||||
db=db,
|
||||
store_id=store.id,
|
||||
role_id=role_id,
|
||||
name=role_data.name,
|
||||
permissions=role_data.permissions,
|
||||
actor_user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
return role
|
||||
|
||||
|
||||
@store_team_router.delete("/roles/{role_id}", status_code=204)
|
||||
def delete_role(
|
||||
request: Request,
|
||||
role_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserContext = Depends(require_store_owner),
|
||||
):
|
||||
"""
|
||||
Delete a custom role.
|
||||
|
||||
**Required:** Store owner only.
|
||||
|
||||
Preset roles cannot be deleted.
|
||||
Roles with assigned team members cannot be deleted.
|
||||
"""
|
||||
store = request.state.store
|
||||
|
||||
store_team_service.delete_role(
|
||||
db=db,
|
||||
store_id=store.id,
|
||||
role_id=role_id,
|
||||
actor_user_id=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Permission Routes
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user