diff --git a/components/renku_data_services/authz/authz.py b/components/renku_data_services/authz/authz.py index 38386204c..cd8f982e9 100644 --- a/components/renku_data_services/authz/authz.py +++ b/components/renku_data_services/authz/authz.py @@ -473,7 +473,7 @@ async def _get_authz_change( authz_change = db_repo.authz._add_project(result) case AuthzOperation.delete, ResourceType.project if isinstance(result, Project): user = _extract_user_from_args(*func_args, **func_kwargs) - authz_change = await db_repo.authz._remove_project(user, result) + authz_change = await db_repo.authz._remove_entity(user, result) case AuthzOperation.delete, ResourceType.project if result is None: # NOTE: This means that the project does not exist in the first place so nothing was deleted pass @@ -489,7 +489,7 @@ async def _get_authz_change( authz_change = db_repo.authz._add_group(result) case AuthzOperation.delete, ResourceType.group if isinstance(result, Group): user = _extract_user_from_args(*func_args, **func_kwargs) - authz_change = await db_repo.authz._remove_group(user, result) + authz_change = await db_repo.authz._remove_entity(user, result) case AuthzOperation.delete, ResourceType.group if result is None: # NOTE: This means that the group does not exist in the first place so nothing was deleted pass @@ -569,6 +569,65 @@ async def decorated_function( return decorator + async def _remove_entity( + self, user: base_models.APIUser, resource: UserInfo | Group | Namespace | Project + ) -> _AuthzChange: + resource_type: ResourceType + match resource: + case _ if isinstance(resource, UserInfo): + resource_type = ResourceType.user + case _ if isinstance(resource, Group): + resource_type = ResourceType.group + case _ if isinstance(resource, Namespace) and resource.kind == NamespaceKind.user: + resource_type = ResourceType.user_namespace + case _ if isinstance(resource, Namespace): + raise errors.ProgrammingError( + message=f"Cannot handle deletetion of namespace {resource.slug} of kind {resource.kind.value}." + ) + case _ if isinstance(resource, Project): + resource_type = ResourceType.project + case _: + raise errors.ProgrammingError(message="Cannot handle deletion of unknown resource.") + resource_id = str(resource.id) + + @_is_allowed_on_resource(Scope.DELETE, resource_type) + async def _remove_entity_wrapped( + authz: Authz, + user: base_models.APIUser, + resource: UserInfo | Group | Namespace | Project, + *, + zed_token: ZedToken | None = None, + ) -> _AuthzChange: + consistency = Consistency(at_least_as_fresh=zed_token) if zed_token else Consistency(fully_consistent=True) + rels: list[Relationship] = [] + # Get relations where the entity is the resource + rel_filter = RelationshipFilter(resource_type=resource_type.value, optional_resource_id=resource_id) + responses: AsyncIterable[ReadRelationshipsResponse] = authz.client.ReadRelationships( + ReadRelationshipsRequest(consistency=consistency, relationship_filter=rel_filter) + ) + async for response in responses: + rels.append(response.relationship) + # Get relations where the entity is the subject + rel_filter = RelationshipFilter( + optional_subject_filter=SubjectFilter(subject_type=resource_type, optional_subject_id=resource_id) + ) + responses: AsyncIterable[ReadRelationshipsResponse] = authz.client.ReadRelationships( + ReadRelationshipsRequest(consistency=consistency, relationship_filter=rel_filter) + ) + async for response in responses: + rels.append(response.relationship) + apply = WriteRelationshipsRequest( + updates=[ + RelationshipUpdate(operation=RelationshipUpdate.OPERATION_DELETE, relationship=i) for i in rels + ] + ) + undo = WriteRelationshipsRequest( + updates=[RelationshipUpdate(operation=RelationshipUpdate.OPERATION_TOUCH, relationship=i) for i in rels] + ) + return _AuthzChange(apply=apply, undo=undo) + + return await _remove_entity_wrapped(self, user, resource) + def _add_project(self, project: Project) -> _AuthzChange: """Create the new project and associated resources and relations in the DB.""" creator = SubjectReference(object=_AuthzConverter.user(project.created_by)) @@ -618,27 +677,6 @@ def _add_project(self, project: Project) -> _AuthzChange: ) return _AuthzChange(apply=apply, undo=undo) - @_is_allowed_on_resource(Scope.DELETE, ResourceType.project) - async def _remove_project( - self, user: base_models.APIUser, project: Project, *, zed_token: ZedToken | None = None - ) -> _AuthzChange: - """Remove the relationships associated with the project.""" - consistency = Consistency(at_least_as_fresh=zed_token) if zed_token else Consistency(fully_consistent=True) - rel_filter = RelationshipFilter(resource_type=ResourceType.project.value, optional_resource_id=str(project.id)) - responses: AsyncIterable[ReadRelationshipsResponse] = self.client.ReadRelationships( - ReadRelationshipsRequest(consistency=consistency, relationship_filter=rel_filter) - ) - rels: list[Relationship] = [] - async for response in responses: - rels.append(response.relationship) - apply = WriteRelationshipsRequest( - updates=[RelationshipUpdate(operation=RelationshipUpdate.OPERATION_DELETE, relationship=i) for i in rels] - ) - undo = WriteRelationshipsRequest( - updates=[RelationshipUpdate(operation=RelationshipUpdate.OPERATION_TOUCH, relationship=i) for i in rels] - ) - return _AuthzChange(apply=apply, undo=undo) - # NOTE changing visibility is the same access level as removal @_is_allowed_on_resource(Scope.DELETE, ResourceType.project) async def _update_project_visibility( @@ -1025,7 +1063,7 @@ def _add_admin(self, user_id: str) -> _AuthzChange: return _AuthzChange(apply=apply, undo=undo) async def _remove_admin(self, user_id: str) -> _AuthzChange: - """Add a deployment-wide administrator in the authorization database.""" + """Remove a deployment-wide administrator from the authorization database.""" existing_admin_ids = await self._get_admin_user_ids() rel = Relationship( resource=_AuthzConverter.platform(), @@ -1086,31 +1124,6 @@ def _add_group(self, group: Group) -> _AuthzChange: ) return _AuthzChange(apply=apply, undo=undo) - @_is_allowed_on_resource(Scope.DELETE, ResourceType.group) - async def _remove_group( - self, user: base_models.APIUser, group: Group, *, zed_token: ZedToken | None = None - ) -> _AuthzChange: - """Remove the group from the authorization database.""" - if not group.id: - raise errors.ProgrammingError( - message="Cannot remove a group in the authorization database if the group has no ID" - ) - consistency = Consistency(at_least_as_fresh=zed_token) if zed_token else Consistency(fully_consistent=True) - rel_filter = RelationshipFilter(resource_type=ResourceType.group.value, optional_resource_id=str(group.id)) - responses = self.client.ReadRelationships( - ReadRelationshipsRequest(consistency=consistency, relationship_filter=rel_filter) - ) - rels: list[Relationship] = [] - async for response in responses: - rels.append(response.relationship) - apply = WriteRelationshipsRequest( - updates=[RelationshipUpdate(operation=RelationshipUpdate.OPERATION_DELETE, relationship=i) for i in rels] - ) - undo = WriteRelationshipsRequest( - updates=[RelationshipUpdate(operation=RelationshipUpdate.OPERATION_TOUCH, relationship=i) for i in rels] - ) - return _AuthzChange(apply=apply, undo=undo) - @_is_allowed(Scope.CHANGE_MEMBERSHIP) async def upsert_group_members( self, @@ -1341,6 +1354,7 @@ def _add_user_namespace(self, namespace: Namespace) -> _AuthzChange: ) return _AuthzChange(apply=apply, undo=undo) + # TODO: remove this method and replace it with _remove_entity() async def _remove_user_namespace(self, user_id: str, zed_token: ZedToken | None = None) -> _AuthzChange: """Remove the user namespace from the authorization database.""" consistency = Consistency(at_least_as_fresh=zed_token) if zed_token else Consistency(fully_consistent=True) diff --git a/test/components/renku_data_services/authz/test_authorization.py b/test/components/renku_data_services/authz/test_authorization.py index 830129be4..24ed07e8e 100644 --- a/test/components/renku_data_services/authz/test_authorization.py +++ b/test/components/renku_data_services/authz/test_authorization.py @@ -76,7 +76,7 @@ async def test_adding_deleting_project(app_config: Config, bootstrap_admins, pub assert not await authz.has_permission(anon_user, ResourceType.project, project_id, Scope.DELETE) assert not await authz.has_permission(regular_user2, ResourceType.project, project_id, Scope.WRITE) assert not await authz.has_permission(regular_user2, ResourceType.project, project_id, Scope.DELETE) - authz_changes = await authz._remove_project(project_owner, project) + authz_changes = await authz._remove_entity(project_owner, project) await authz.client.WriteRelationships(authz_changes.apply) assert not await authz.has_permission(admin_user, ResourceType.project, project_id, Scope.READ) assert not await authz.has_permission(admin_user, ResourceType.project, project_id, Scope.WRITE) @@ -291,7 +291,7 @@ async def test_listing_projects_with_access(app_config: Config, bootstrap_admins == 0 ) # Test project deletion - changes = await authz._remove_project(project_owner, private_project1) + changes = await authz._remove_entity(project_owner, private_project1) await authz.client.WriteRelationships(changes.apply) assert private_project_id1_str not in set( await authz.resources_with_permission(admin_user, project_owner.id, ResourceType.project, Scope.READ)