diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index ab1837274da..bc4710abb93 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -504,6 +504,9 @@ type VirtualFolder implements Item { group: UUID group_name: String creator: String + + """Added in 24.09.0.""" + domain_name: String unmanaged_path: String usage_mode: String permission: String diff --git a/src/ai/backend/manager/models/acl.py b/src/ai/backend/manager/models/acl.py index 46855d448de..1ec16993b6a 100644 --- a/src/ai/backend/manager/models/acl.py +++ b/src/ai/backend/manager/models/acl.py @@ -92,6 +92,23 @@ class AbstractScopePermissionMap(Generic[ACLPermissionType, ACLObjectType], meta project: Mapping[uuid.UUID, frozenset[ACLPermissionType]] domain: Mapping[str, frozenset[ACLPermissionType]] + def apply_permission_filter(self, permission_to_include: ACLPermissionType) -> None: + self.user = { + uid: permissions + for uid, permissions in self.user.items() + if permission_to_include in permissions + } + self.project = { + pid: permissions + for pid, permissions in self.project.items() + if permission_to_include in permissions + } + self.domain = { + dname: permissions + for dname, permissions in self.domain.items() + if permission_to_include in permissions + } + @abstractmethod async def determine_permission(self, acl_obj: ACLObjectType) -> frozenset[ACLPermissionType]: pass @@ -100,7 +117,7 @@ async def determine_permission(self, acl_obj: ACLObjectType) -> frozenset[ACLPer ScopePermissionMapType = TypeVar("ScopePermissionMapType", bound=AbstractScopePermissionMap) -class AbstractScopePermissionMapBuilder(Generic[ScopePermissionMapType], metaclass=ABCMeta): +class AbstractScopePermissionMapFactory(Generic[ScopePermissionMapType], metaclass=ABCMeta): @classmethod async def build( cls, @@ -110,14 +127,26 @@ async def build( ) -> ScopePermissionMapType: match requested_scope: case RequestedUserScope(user_id=user_id): - return await cls._build_in_user_scope(db_session, ctx, user_id) + result = await cls._build_in_user_scope( + db_session, + ctx, + user_id, + ) case RequestedProjectScope(project_id=project_id): - return await cls._build_in_project_scope(db_session, ctx, project_id) + result = await cls._build_in_project_scope( + db_session, + ctx, + project_id, + ) case RequestedDomainScope(domain_name=domain_name): - return await cls._build_in_domain_scope(db_session, ctx, domain_name) + result = await cls._build_in_domain_scope( + db_session, + ctx, + domain_name, + ) case _: - pass - raise RuntimeError(f"invalid request scope {requested_scope}") + raise RuntimeError(f"invalid request scope {requested_scope}") + return result @classmethod @abstractmethod diff --git a/src/ai/backend/manager/models/vfolder.py b/src/ai/backend/manager/models/vfolder.py index 60fb8a3625a..99935ba7277 100644 --- a/src/ai/backend/manager/models/vfolder.py +++ b/src/ai/backend/manager/models/vfolder.py @@ -57,7 +57,7 @@ from .acl import ( AbstractACLPermission, AbstractScopePermissionMap, - AbstractScopePermissionMapBuilder, + AbstractScopePermissionMapFactory, RequestedScope, RequesterContext, ) @@ -127,7 +127,7 @@ "VFolderACLObject", "OWNER_PERMISSIONS", "ScopePermissionMap", - "ScopePermissionMapBuilder", + "ScopePermissionMapFactory", ) @@ -742,10 +742,23 @@ class ScopePermissionMap(AbstractScopePermissionMap[VFolderACLPermission, VFolde additional: Mapping[ uuid.UUID, frozenset[VFolderACLPermission] ] # Key: vfolder id / Value: set of VFolderACLPermissions - overriden: Mapping[ + overridden: Mapping[ uuid.UUID, frozenset[VFolderACLPermission] ] # Key: vfolder id / Value: set of VFolderACLPermissions + def apply_permission_filter(self, permission_to_include: VFolderACLPermission) -> None: + super().apply_permission_filter(permission_to_include) + self.additional = { + vfid: permissions + for vfid, permissions in self.additional.items() + if permission_to_include in permissions + } + self.overridden = { + vfid: permissions + for vfid, permissions in self.overridden.items() + if permission_to_include in permissions + } + @property def query_condition(self) -> WhereClauseType | None: cond: WhereClauseType | None = None @@ -764,8 +777,8 @@ def _coalesce( cond = _coalesce(cond, VFolderRow.domain_name.in_(self.domain.keys())) if self.additional: cond = _coalesce(cond, VFolderRow.id.in_(self.additional.keys())) - if self.overriden: - cond = _coalesce(cond, VFolderRow.id.in_(self.overriden.keys())) + if self.overridden: + cond = _coalesce(cond, VFolderRow.id.in_(self.overridden.keys())) return cond @property @@ -777,8 +790,8 @@ def query_stmt(self) -> sa.sql.Select | None: async def determine_permission(self, acl_obj: VFolderRow) -> frozenset[VFolderACLPermission]: vfolder_row = acl_obj - if (vfolder_id := acl_obj.id) in self.overriden: - return self.overriden[vfolder_id] + if (vfolder_id := acl_obj.id) in self.overridden: + return self.overridden[vfolder_id] permissions: set[VFolderACLPermission] = set() permissions |= self.additional.get(vfolder_id, set()) permissions |= self.user.get(vfolder_row.user, set()) @@ -787,7 +800,7 @@ async def determine_permission(self, acl_obj: VFolderRow) -> frozenset[VFolderAC return frozenset(permissions) -class ScopePermissionMapBuilder(AbstractScopePermissionMapBuilder[ScopePermissionMap]): +class ScopePermissionMapFactory(AbstractScopePermissionMapFactory[ScopePermissionMap]): @classmethod async def _build_in_user_scope( cls, @@ -799,7 +812,7 @@ async def _build_in_user_scope( project: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} domain: Mapping[str, frozenset[VFolderACLPermission]] = {} additional: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} - overriden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} + overridden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} match ctx.user_role: case UserRole.SUPERADMIN: if ctx.user_id == user_id: @@ -871,7 +884,7 @@ async def _build_in_user_scope( user = {user_id: DEFAULT_ADMIN_PERMISSIONS_ON_USER_VFOLDERS} case UserRole.USER: if ctx.user_id == user_id: - overriden_stmt = ( + overridden_stmt = ( sa.select(VFolderPermissionRow) .select_from(sa.join(VFolderPermissionRow, VFolderRow)) .where( @@ -881,9 +894,9 @@ async def _build_in_user_scope( ) # filter out project vfolders ) ) - overriden = { + overridden = { row.vfolder: PERMISSION_TO_ACL_PERMISSION_MAP[row.permission] - for row in await db_session.scalars(overriden_stmt) + for row in await db_session.scalars(overridden_stmt) } user = {ctx.user_id: OWNER_PERMISSIONS} @@ -894,7 +907,7 @@ async def _build_in_user_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) @classmethod @@ -908,7 +921,7 @@ async def _build_in_project_scope( project: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} domain: Mapping[str, frozenset[VFolderACLPermission]] = {} additional: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} - overriden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} + overridden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} project_ctx = await ctx.get_or_init_project_ctx() role_in_project = project_ctx.get(project_id) @@ -986,7 +999,7 @@ async def _build_in_project_scope( } # Admin/user is associated with the project, fetch owned/invited vfolders - overriden_stmt = ( + overridden_stmt = ( sa.select(VFolderPermissionRow) .select_from(sa.join(VFolderPermissionRow, VFolderRow)) .where( @@ -998,9 +1011,9 @@ async def _build_in_project_scope( # project vfolders or invited user vfolders ) ) - overriden = { + overridden = { row.vfolder: PERMISSION_TO_ACL_PERMISSION_MAP[row.permission] - for row in await db_session.scalars(overriden_stmt) + for row in await db_session.scalars(overridden_stmt) } user = {ctx.user_id: OWNER_PERMISSIONS} @@ -1014,7 +1027,7 @@ async def _build_in_project_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) @classmethod @@ -1028,7 +1041,7 @@ async def _build_in_domain_scope( project: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} domain: Mapping[str, frozenset[VFolderACLPermission]] = {} additional: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} - overriden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} + overridden: Mapping[uuid.UUID, frozenset[VFolderACLPermission]] = {} match ctx.user_role: case UserRole.SUPERADMIN: domain = { @@ -1044,7 +1057,7 @@ async def _build_in_domain_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) project_ctx = await ctx.get_or_init_project_ctx() project = { @@ -1088,7 +1101,7 @@ async def _build_in_domain_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) project_ctx = await ctx.get_or_init_project_ctx() project = { @@ -1140,7 +1153,7 @@ async def _build_in_domain_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) project_ctx = await ctx.get_or_init_project_ctx() project = { @@ -1149,7 +1162,7 @@ async def _build_in_domain_scope( else OWNER_PERMISSIONS for project_id, role in project_ctx.items() } - overriden_stmt = ( + overridden_stmt = ( sa.select(VFolderPermissionRow) .select_from(sa.join(VFolderPermissionRow, VFolderRow)) .where( @@ -1157,9 +1170,9 @@ async def _build_in_domain_scope( & (VFolderRow.domain_name == domain_name) ) ) - overriden = { + overridden = { row.vfolder: PERMISSION_TO_ACL_PERMISSION_MAP[row.permission] - for row in await db_session.scalars(overriden_stmt) + for row in await db_session.scalars(overridden_stmt) } user = {ctx.user_id: OWNER_PERMISSIONS} @@ -1171,7 +1184,7 @@ async def _build_in_domain_scope( project=project, domain=domain, additional=additional, - overriden=overriden, + overridden=overridden, ) @@ -1192,7 +1205,9 @@ async def get_vfolders( blocked_status: Container[VFolderOperationStatus] | None = None, ) -> list[VFolderACLObject]: async with SASession(ctx.db_conn) as db_session: - scope_ctx = await ScopePermissionMapBuilder.build(db_session, ctx, requested_scope) + scope_ctx = await ScopePermissionMapFactory.build(db_session, ctx, requested_scope) + if requested_permission is not None: + scope_ctx.apply_permission_filter(requested_permission) stmt = scope_ctx.query_stmt if stmt is None: return [] @@ -1210,8 +1225,7 @@ async def get_vfolders( result: list[VFolderACLObject] = [] for row in await db_session.scalars(stmt): permissions = await scope_ctx.determine_permission(row) - if requested_permission is None or requested_permission in permissions: - result.append(VFolderACLObject(row, permissions)) + result.append(VFolderACLObject(row, permissions)) return result