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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user