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

@@ -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
# ============================================================================