-
Notifications
You must be signed in to change notification settings - Fork 171
test(BA-5375): add RBAC registry completeness test #10534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Central RBAC action registry list. | ||
|
|
||
| This module provides the canonical list of all RBAC action classes | ||
| that should be registered in the PermissionControllerService. | ||
| Keeping this list in a lightweight module allows tests to verify | ||
| registry completeness without importing the full service factory. | ||
| """ | ||
|
|
||
| from ai.backend.manager.actions.action.rbac import BaseRBACAction | ||
| from ai.backend.manager.actions.action.rbac_session import ( | ||
| SessionCreateRBACAction, | ||
| SessionGetRBACAction, | ||
| SessionGrantAllRBACAction, | ||
| SessionGrantHardDeleteRBACAction, | ||
| SessionGrantReadRBACAction, | ||
| SessionGrantUpdateRBACAction, | ||
| SessionHardDeleteRBACAction, | ||
| SessionSearchRBACAction, | ||
| SessionUpdateRBACAction, | ||
| ) | ||
|
|
||
| RBAC_ACTION_REGISTRY: list[type[BaseRBACAction]] = [ | ||
| SessionCreateRBACAction, | ||
| SessionGetRBACAction, | ||
| SessionSearchRBACAction, | ||
| SessionUpdateRBACAction, | ||
| SessionHardDeleteRBACAction, | ||
| SessionGrantAllRBACAction, | ||
| SessionGrantReadRBACAction, | ||
| SessionGrantUpdateRBACAction, | ||
| SessionGrantHardDeleteRBACAction, | ||
| ] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||||||
| """Tests for RBAC action registry completeness.""" | ||||||||||||
|
|
||||||||||||
| import importlib | ||||||||||||
| import inspect | ||||||||||||
| import pkgutil | ||||||||||||
|
|
||||||||||||
| import ai.backend.manager.actions.action as action_pkg | ||||||||||||
| from ai.backend.manager.actions.action.rbac import BaseRBACAction | ||||||||||||
| from ai.backend.manager.actions.action.rbac_registry import RBAC_ACTION_REGISTRY | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _import_all_rbac_modules() -> None: | ||||||||||||
| """Import all rbac_* modules to ensure __subclasses__() discovers everything.""" | ||||||||||||
| package_path = action_pkg.__path__ | ||||||||||||
| for module_info in pkgutil.iter_modules(package_path): | ||||||||||||
| if module_info.name.startswith("rbac_"): | ||||||||||||
| importlib.import_module(f"{action_pkg.__name__}.{module_info.name}") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _collect_concrete_subclasses(base: type) -> set[type[BaseRBACAction]]: | ||||||||||||
| """Recursively collect all concrete (non-abstract) subclasses.""" | ||||||||||||
| result: set[type[BaseRBACAction]] = set() | ||||||||||||
| for sub in base.__subclasses__(): | ||||||||||||
| if not inspect.isabstract(sub): | ||||||||||||
| result.add(sub) | ||||||||||||
| result.update(_collect_concrete_subclasses(sub)) | ||||||||||||
| return result | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| # Ensure all rbac_* modules are imported before any test runs. | ||||||||||||
| _import_all_rbac_modules() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class TestRBACRegistryCompleteness: | ||||||||||||
| def test_all_concrete_subclasses_are_registered(self) -> None: | ||||||||||||
| concrete_subclasses = _collect_concrete_subclasses(BaseRBACAction) | ||||||||||||
| registry_set = set(RBAC_ACTION_REGISTRY) | ||||||||||||
| missing = concrete_subclasses - registry_set | ||||||||||||
| assert not missing, ( | ||||||||||||
| f"The following BaseRBACAction subclasses are not in RBAC_ACTION_REGISTRY: " | ||||||||||||
| f"{', '.join(cls.__name__ for cls in sorted(missing, key=lambda c: c.__name__))}" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| def test_registry_has_no_duplicates(self) -> None: | ||||||||||||
| seen: set[type[BaseRBACAction]] = set() | ||||||||||||
| duplicates: list[str] = [] | ||||||||||||
| for cls in RBAC_ACTION_REGISTRY: | ||||||||||||
| if cls in seen: | ||||||||||||
| duplicates.append(cls.__name__) | ||||||||||||
| seen.add(cls) | ||||||||||||
| assert not duplicates, f"Duplicate entries in RBAC_ACTION_REGISTRY: {', '.join(duplicates)}" | ||||||||||||
|
|
||||||||||||
| def test_registry_contains_only_base_rbac_action_subclasses(self) -> None: | ||||||||||||
| non_subclasses: list[str] = [] | ||||||||||||
| for entry in RBAC_ACTION_REGISTRY: | ||||||||||||
|
||||||||||||
| for entry in RBAC_ACTION_REGISTRY: | |
| for entry in RBAC_ACTION_REGISTRY: | |
| if not isinstance(entry, type): | |
| non_subclasses.append(repr(entry)) | |
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RBAC_ACTION_REGISTRYis a module-level constant but is defined as a mutablelist. Sincefactory.create_services()passes this object through directly, any accidental mutation would affect all consumers (and could create hard-to-debug global state). Consider making it immutable (e.g.,Final+tuple[...]) and type it as aSequence[type[BaseRBACAction]]/tuple[...]to communicate intent and prevent mutation.