-
Notifications
You must be signed in to change notification settings - Fork 164
feat(BA-2274): Apply RBAC validation to VFolder #6360
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 @@ | ||
| Apply RBAC validators to basic VFolder actions |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from dataclasses import dataclass | ||
|
|
||
| from .batch import BatchActionValidator | ||
| from .scope import ScopeActionValidator | ||
| from .single_entity import SingleEntityActionValidator | ||
|
|
||
|
|
||
| @dataclass | ||
| class ValidatorArgs: | ||
| batch: list[BatchActionValidator] | ||
| scope: list[ScopeActionValidator] | ||
| single_entity: list[SingleEntityActionValidator] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from aiohttp import web | ||
|
|
||
| from ai.backend.common.exception import ( | ||
| BackendAIError, | ||
| ErrorCode, | ||
| ErrorDetail, | ||
| ErrorDomain, | ||
| ErrorOperation, | ||
| ) | ||
|
|
||
|
|
||
| class RBACForbidden(BackendAIError, web.HTTPForbidden): | ||
| error_type = "https://api.backend.ai/probs/rbac-forbidden" | ||
| error_title = "The operation is forbidden due to insufficient RBAC permissions." | ||
|
|
||
| @classmethod | ||
| def error_code(cls) -> ErrorCode: | ||
| return ErrorCode( | ||
| domain=ErrorDomain.PERMISSION, | ||
| operation=ErrorOperation.ACCESS, | ||
| error_detail=ErrorDetail.FORBIDDEN, | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Mapping | ||||||||||||||||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||||||||||||||||
| from typing import Optional, Self | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| from ai.backend.common.exception import BackendAIError | ||||||||||||||||||||||||||||||||||||||||||
| from ai.backend.common.metrics.metric import DomainType, LayerType | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,6 +20,7 @@ | |||||||||||||||||||||||||||||||||||||||||
| UserRoleAssignmentInput, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| from ...models.utils import ExtendedAsyncSAEngine | ||||||||||||||||||||||||||||||||||||||||||
| from ..types import RepositoryArgs | ||||||||||||||||||||||||||||||||||||||||||
| from .db_source import PermissionDBSource | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| permission_controller_repository_resilience = Resilience( | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,6 +48,12 @@ class PermissionControllerRepository: | |||||||||||||||||||||||||||||||||||||||||
| def __init__(self, db: ExtendedAsyncSAEngine) -> None: | ||||||||||||||||||||||||||||||||||||||||||
| self._db_source = PermissionDBSource(db) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||
| def create(cls, args: RepositoryArgs) -> Self: | ||||||||||||||||||||||||||||||||||||||||||
| return cls( | ||||||||||||||||||||||||||||||||||||||||||
| db=args.db, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @permission_controller_repository_resilience.apply() | ||||||||||||||||||||||||||||||||||||||||||
| async def create_role(self, data: RoleCreateInput) -> RoleData: | ||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,27 +86,20 @@ async def get_role(self, role_id: uuid.UUID) -> Optional[RoleData]: | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @permission_controller_repository_resilience.apply() | ||||||||||||||||||||||||||||||||||||||||||
| async def check_permission_of_entity(self, data: SingleEntityPermissionCheckInput) -> bool: | ||||||||||||||||||||||||||||||||||||||||||
| target_object_id = data.target_object_id | ||||||||||||||||||||||||||||||||||||||||||
| roles = await self._db_source.get_user_roles(data.user_id) | ||||||||||||||||||||||||||||||||||||||||||
| associated_scopes = await self._db_source.get_entity_mapped_scopes(target_object_id) | ||||||||||||||||||||||||||||||||||||||||||
| associated_scopes_set = set([row.parsed_scope_id() for row in associated_scopes]) | ||||||||||||||||||||||||||||||||||||||||||
| for role in roles: | ||||||||||||||||||||||||||||||||||||||||||
| for object_perm in role.object_permission_rows: | ||||||||||||||||||||||||||||||||||||||||||
| if object_perm.operation != data.operation: | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
| if object_perm.object_id() == target_object_id: | ||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| for permission_group in role.permission_group_rows: | ||||||||||||||||||||||||||||||||||||||||||
| if permission_group.parsed_scope_id() not in associated_scopes_set: | ||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||
| for permission in permission_group.permission_rows: | ||||||||||||||||||||||||||||||||||||||||||
| if permission.operation == data.operation: | ||||||||||||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||
| Check if the user has the requested operation permission on the given entity. | ||||||||||||||||||||||||||||||||||||||||||
| Returns True if the permission exists, False otherwise. | ||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||
| return await self._db_source.check_object_permission_exist( | ||||||||||||||||||||||||||||||||||||||||||
| data.user_id, data.target_object_id, data.operation | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+93
to
+95
|
||||||||||||||||||||||||||||||||||||||||||
| return await self._db_source.check_object_permission_exist( | |
| data.user_id, data.target_object_id, data.operation | |
| ) | |
| # Comprehensive permission check: direct object permissions and scope-based permission groups | |
| # 1. Get all roles assigned to the user | |
| roles = await self._db_source.get_roles_of_user(data.user_id) | |
| for role in roles: | |
| # 2. Check direct object permissions for the role | |
| if await self._db_source.check_object_permission_exist( | |
| role.id, data.target_object_id, data.operation | |
| ): | |
| return True | |
| # 3. Check permission groups (scope-based) | |
| permission_groups = await self._db_source.get_permission_groups_of_role(role.id) | |
| for group in permission_groups: | |
| if await self._db_source.check_scope_permission_exist( | |
| group.scope_id, data.target_object_id, data.operation | |
| ): | |
| return True | |
| return False |
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.
How about juwt
validrather thanis_valid.