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

@@ -179,3 +179,82 @@ class TestMenuDiscoveryServiceEnabledModuleCodes:
assert "billing" in section_ids
assert "loyalty" in section_ids
assert "account" in section_ids
@pytest.mark.unit
@pytest.mark.core
class TestMenuDiscoveryPermissionFiltering:
"""Test user_permissions parameter on get_menu_for_frontend."""
def setup_method(self):
self.service = MenuDiscoveryService()
def test_no_permission_filter_shows_all_items(self, db):
"""When user_permissions is None, no permission filtering occurs."""
enabled = {"core", "catalog", "orders", "tenancy", "billing", "loyalty"}
sections = self.service.get_menu_for_frontend(
db, FrontendType.STORE, enabled_module_codes=enabled,
user_permissions=None,
)
# Should have items regardless of requires_permission
all_items = [item for s in sections for item in s.items]
assert len(all_items) > 0
def test_permission_filter_hides_unpermitted_items(self, db):
"""Items with requires_permission not in user_permissions are hidden."""
enabled = {"core", "catalog", "orders", "tenancy", "billing", "loyalty"}
# Only allow dashboard.view — should hide products, orders, etc.
sections = self.service.get_menu_for_frontend(
db, FrontendType.STORE, enabled_module_codes=enabled,
user_permissions=["dashboard.view"],
)
all_item_ids = {item.id for s in sections for item in s.items}
# Dashboard should be visible (has requires_permission="dashboard.view")
assert "dashboard" in all_item_ids
# Products requires "products.view" — should be hidden
assert "products" not in all_item_ids
def test_permission_filter_shows_permitted_items(self, db):
"""Items with matching permission are shown."""
enabled = {"core", "catalog", "orders", "tenancy"}
sections = self.service.get_menu_for_frontend(
db, FrontendType.STORE, enabled_module_codes=enabled,
user_permissions=["dashboard.view", "products.view", "orders.view", "team.view"],
)
all_item_ids = {item.id for s in sections for item in s.items}
assert "dashboard" in all_item_ids
assert "products" in all_item_ids
assert "orders" in all_item_ids
def test_empty_permissions_list_hides_permission_required_items(self, db):
"""Empty permissions list hides all items that require a permission."""
enabled = {"core", "catalog"}
sections = self.service.get_menu_for_frontend(
db, FrontendType.STORE, enabled_module_codes=enabled,
user_permissions=[],
)
all_item_ids = {item.id for s in sections for item in s.items}
# Items without requires_permission should still show
# Items with requires_permission should be hidden
assert "products" not in all_item_ids
assert "dashboard" not in all_item_ids
def test_items_without_requires_permission_always_visible(self, db):
"""Items that have no requires_permission are shown regardless of user_permissions."""
enabled = {"core", "catalog", "tenancy"}
# Get all store items to check which have no requires_permission
all_items_raw = self.service.get_all_menu_items(FrontendType.STORE)
items_without_perm = [
item for item in all_items_raw if item.requires_permission is None
]
if items_without_perm:
sections = self.service.get_menu_for_frontend(
db, FrontendType.STORE, enabled_module_codes=enabled,
user_permissions=["some.random.permission"],
)
all_item_ids = {item.id for s in sections for item in s.items}
# Items without requires_permission should still be visible
for item in items_without_perm:
if item.module_code in enabled:
assert item.id in all_item_ids