diff --git a/src/specklepy/api/resources/current/active_user_resource.py b/src/specklepy/api/resources/current/active_user_resource.py index 3d629da..c5fe268 100644 --- a/src/specklepy/api/resources/current/active_user_resource.py +++ b/src/specklepy/api/resources/current/active_user_resource.py @@ -11,7 +11,11 @@ from specklepy.core.api.models import ( ResourceCollection, User, ) -from specklepy.core.api.models.current import PermissionCheckResult, Workspace +from specklepy.core.api.models.current import ( + PermissionCheckResult, + ProjectWithPermissions, + Workspace, +) from specklepy.core.api.resources import ActiveUserResource as CoreResource from specklepy.logging import metrics @@ -51,6 +55,22 @@ class ActiveUserResource(CoreResource): metrics.track(metrics.SDK, self.account, {"name": "Active User Get Projects"}) return super().get_projects(limit=limit, cursor=cursor, filter=filter) + def get_projects_with_permissions( + self, + *, + limit: int = 25, + cursor: Optional[str] = None, + filter: Optional[UserProjectsFilter] = None, + ) -> ResourceCollection[ProjectWithPermissions]: + metrics.track( + metrics.SDK, + self.account, + {"name": "Active User Get Projects With Permissions"}, + ) + return super().get_projects_with_permissions( + limit=limit, cursor=cursor, filter=filter + ) + def get_project_invites(self) -> List[PendingStreamCollaborator]: metrics.track( metrics.SDK, self.account, {"name": "Active User Get Project Invites"} diff --git a/src/specklepy/api/resources/current/project_resource.py b/src/specklepy/api/resources/current/project_resource.py index 9904972..125ab3f 100644 --- a/src/specklepy/api/resources/current/project_resource.py +++ b/src/specklepy/api/resources/current/project_resource.py @@ -7,7 +7,11 @@ from specklepy.core.api.inputs.project_inputs import ( ProjectUpdateRoleInput, WorkspaceProjectCreateInput, ) -from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam +from specklepy.core.api.models import ( + Project, + ProjectWithModels, + ProjectWithTeam, +) from specklepy.core.api.models.current import ProjectPermissionChecks from specklepy.core.api.resources import ProjectResource as CoreResource from specklepy.logging import metrics diff --git a/src/specklepy/api/resources/current/workspace_resource.py b/src/specklepy/api/resources/current/workspace_resource.py index 13d1043..71a764d 100644 --- a/src/specklepy/api/resources/current/workspace_resource.py +++ b/src/specklepy/api/resources/current/workspace_resource.py @@ -1,7 +1,12 @@ from typing import Optional from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter -from specklepy.core.api.models.current import Project, ResourceCollection, Workspace +from specklepy.core.api.models.current import ( + Project, + ProjectWithPermissions, + ResourceCollection, + Workspace, +) from specklepy.core.api.resources import WorkspaceResource as CoreResource from specklepy.logging import metrics @@ -30,3 +35,19 @@ class WorkspaceResource(CoreResource): ) -> ResourceCollection[Project]: metrics.track(metrics.SDK, self.account, {"name": "Workspace Get Projects"}) return super().get_projects(workspace_id, limit, cursor, filter) + + def get_projects_with_permissions( + self, + workspace_id: str, + limit: int = 25, + cursor: Optional[str] = None, + filter: Optional[WorksaceProjectsFilter] = None, + ) -> ResourceCollection[ProjectWithPermissions]: + metrics.track( + metrics.SDK, + self.account, + {"name": "Workspace Get Projects With Permissions"}, + ) + return super().get_projects_with_permissions( + workspace_id, limit, cursor, filter + ) diff --git a/src/specklepy/core/api/models/__init__.py b/src/specklepy/core/api/models/__init__.py index ce54cb8..737d234 100644 --- a/src/specklepy/core/api/models/__init__.py +++ b/src/specklepy/core/api/models/__init__.py @@ -8,6 +8,7 @@ from specklepy.core.api.models.current import ( ProjectCollaborator, ProjectCommentCollection, ProjectWithModels, + ProjectWithPermissions, ProjectWithTeam, ResourceCollection, ServerConfiguration, @@ -39,6 +40,7 @@ __all__ = [ "ModelWithVersions", "Project", "ProjectWithModels", + "ProjectWithPermissions", "ProjectWithTeam", "ProjectCommentCollection", "UserSearchResultCollection", diff --git a/src/specklepy/core/api/models/current.py b/src/specklepy/core/api/models/current.py index 9a55617..499484d 100644 --- a/src/specklepy/core/api/models/current.py +++ b/src/specklepy/core/api/models/current.py @@ -176,6 +176,10 @@ class ProjectWithModels(Project): models: ResourceCollection[Model] +class ProjectWithPermissions(Project): + permissions: ProjectPermissionChecks + + class ProjectWithTeam(Project): invited_team: List[PendingStreamCollaborator] team: List[ProjectCollaborator] diff --git a/src/specklepy/core/api/resources/current/active_user_resource.py b/src/specklepy/core/api/resources/current/active_user_resource.py index 0f495dc..4a2c27b 100644 --- a/src/specklepy/core/api/resources/current/active_user_resource.py +++ b/src/specklepy/core/api/resources/current/active_user_resource.py @@ -13,7 +13,11 @@ from specklepy.core.api.models import ( ResourceCollection, User, ) -from specklepy.core.api.models.current import PermissionCheckResult, Workspace +from specklepy.core.api.models.current import ( + PermissionCheckResult, + ProjectWithPermissions, + Workspace, +) from specklepy.core.api.resource import ResourceBase from specklepy.core.api.responses import DataResponse from specklepy.logging.exceptions import GraphQLException @@ -338,3 +342,84 @@ class ActiveUserResource(ResourceBase): ) return response.data.data + + def get_projects_with_permissions( + self, + *, + limit: int = 25, + cursor: Optional[str] = None, + filter: Optional[UserProjectsFilter] = None, + ) -> ResourceCollection[ProjectWithPermissions]: + """ + Gets the currently active user's projects with their permissions. + This is useful for checking what actions can be performed on each project. + """ + QUERY = gql( + """ + query User($limit : Int!, $cursor: String, $filter: UserProjectsFilter) { + data:activeUser { + data:projects(limit: $limit, cursor: $cursor, filter: $filter) { + totalCount + cursor + items { + id + name + description + visibility + allowPublicComments + role + createdAt + updatedAt + sourceApps + workspaceId + permissions { + canCreateModel { + code + authorized + message + } + canDelete { + code + authorized + message + } + canLoad { + code + authorized + message + } + canPublish { + code + authorized + message + } + } + } + } + } + } + """ + ) + + variables = { + "limit": limit, + "cursor": cursor, + "filter": filter.model_dump(warnings="error", by_alias=True) + if filter + else None, + } + + response = self.make_request_and_parse_response( + DataResponse[ + Optional[DataResponse[ResourceCollection[ProjectWithPermissions]]] + ], + QUERY, + variables, + ) + + if response.data is None: + raise GraphQLException( + "GraphQL response indicated that the ActiveUser could not be found" + ) + + return response.data.data diff --git a/src/specklepy/core/api/resources/current/project_resource.py b/src/specklepy/core/api/resources/current/project_resource.py index 7e3a92c..b2a5e6f 100644 --- a/src/specklepy/core/api/resources/current/project_resource.py +++ b/src/specklepy/core/api/resources/current/project_resource.py @@ -9,7 +9,11 @@ from specklepy.core.api.inputs.project_inputs import ( ProjectUpdateRoleInput, WorkspaceProjectCreateInput, ) -from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam +from specklepy.core.api.models import ( + Project, + ProjectWithModels, + ProjectWithTeam, +) from specklepy.core.api.models.current import ProjectPermissionChecks from specklepy.core.api.resource import ResourceBase from specklepy.core.api.responses import DataResponse diff --git a/src/specklepy/core/api/resources/current/workspace_resource.py b/src/specklepy/core/api/resources/current/workspace_resource.py index e210d42..1317a04 100644 --- a/src/specklepy/core/api/resources/current/workspace_resource.py +++ b/src/specklepy/core/api/resources/current/workspace_resource.py @@ -3,7 +3,12 @@ from typing import Optional from gql import gql from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter -from specklepy.core.api.models.current import Project, ResourceCollection, Workspace +from specklepy.core.api.models.current import ( + Project, + ProjectWithPermissions, + ResourceCollection, + Workspace, +) from specklepy.core.api.resource import ResourceBase from specklepy.core.api.responses import DataResponse @@ -104,3 +109,72 @@ class WorkspaceResource(ResourceBase): return self.make_request_and_parse_response( DataResponse[DataResponse[ResourceCollection[Project]]], QUERY, variables ).data.data + + def get_projects_with_permissions( + self, + workspace_id: str, + limit: int = 25, + cursor: Optional[str] = None, + filter: Optional[WorksaceProjectsFilter] = None, + ) -> ResourceCollection[ProjectWithPermissions]: + QUERY = gql( + """ + query Workspace($workspaceId: String!, $limit: Int!, $cursor: String, $filter: WorkspaceProjectsFilter) { + data:workspace(id: $workspaceId) { + data:projects(limit: $limit, cursor: $cursor, filter: $filter) { + cursor + items { + allowPublicComments + createdAt + description + id + name + role + sourceApps + updatedAt + visibility + workspaceId + permissions { + canCreateModel { + code + authorized + message + } + canDelete { + code + authorized + message + } + canLoad { + code + authorized + message + } + canPublish { + code + authorized + message + } + } + } + totalCount + } + } + } + """ # noqa: E501 + ) + + variables = { + "workspaceId": workspace_id, + "limit": limit, + "cursor": cursor, + "filter": filter.model_dump(warnings="error", by_alias=True) + if filter + else None, + } + + return self.make_request_and_parse_response( + DataResponse[DataResponse[ResourceCollection[ProjectWithPermissions]]], + QUERY, + variables, + ).data.data diff --git a/tests/integration/client/current/test_active_user_resource_permissions.py b/tests/integration/client/current/test_active_user_resource_permissions.py new file mode 100644 index 0000000..86816d8 --- /dev/null +++ b/tests/integration/client/current/test_active_user_resource_permissions.py @@ -0,0 +1,85 @@ +import pytest + +from specklepy.api.client import SpeckleClient +from specklepy.core.api.inputs.project_inputs import ProjectCreateInput +from specklepy.core.api.inputs.user_inputs import UserProjectsFilter +from specklepy.core.api.models.current import ( + Project, + ProjectWithPermissions, + ResourceCollection, +) + + +@pytest.mark.run() +class TestActiveUserResourcePermissions: + @pytest.fixture() + def test_project(self, client: SpeckleClient) -> Project: + project = client.project.create( + ProjectCreateInput( + name="test project for active user permissions", + description="test description", + visibility=None, + ) + ) + return project + + def test_active_user_get_projects_with_permissions( + self, client: SpeckleClient, test_project: Project + ): + result = client.active_user.get_projects_with_permissions() + + assert isinstance(result, ResourceCollection) + assert len(result.items) >= 1 + + test_project_with_permissions = None + for project in result.items: + if project.id == test_project.id: + test_project_with_permissions = project + break + + assert test_project_with_permissions is not None + assert isinstance(test_project_with_permissions, ProjectWithPermissions) + + assert hasattr(test_project_with_permissions, "permissions") + assert test_project_with_permissions.permissions is not None + + assert test_project_with_permissions.id == test_project.id + assert test_project_with_permissions.name == test_project.name + + permissions = test_project_with_permissions.permissions + assert hasattr(permissions, "can_create_model") + assert hasattr(permissions, "can_delete") + assert hasattr(permissions, "can_load") + assert hasattr(permissions, "can_publish") + + assert permissions.can_create_model.authorized is True + assert permissions.can_delete.authorized is True + assert permissions.can_load.authorized is True + assert permissions.can_publish.authorized is True + + def test_active_user_get_projects_with_permissions_with_filter( + self, client: SpeckleClient, test_project: Project + ): + """test getting active user's projects with permissions using a filter.""" + filter = UserProjectsFilter(search=test_project.name) + + result = client.active_user.get_projects_with_permissions(filter=filter) + + assert isinstance(result, ResourceCollection) + assert len(result.items) >= 1 + assert result.total_count >= 1 + + project_with_permissions = result.items[0] + assert isinstance(project_with_permissions, ProjectWithPermissions) + assert project_with_permissions.id == test_project.id + + assert hasattr(project_with_permissions, "permissions") + assert project_with_permissions.permissions is not None + + def test_active_user_projects_with_permissions_method_exists( + self, client: SpeckleClient + ): + """test that the method exists and is callable on active user resource.""" + assert hasattr(client.active_user, "get_projects_with_permissions") + method = client.active_user.get_projects_with_permissions + assert callable(method) diff --git a/tests/integration/client/current/test_workspace_resource_permissions.py b/tests/integration/client/current/test_workspace_resource_permissions.py new file mode 100644 index 0000000..4d6f145 --- /dev/null +++ b/tests/integration/client/current/test_workspace_resource_permissions.py @@ -0,0 +1,19 @@ +import pytest + +from specklepy.api.client import SpeckleClient +from specklepy.logging.exceptions import GraphQLException + + +@pytest.mark.run() +class TestWorkspaceResourcePermissions: + def test_get_projects_with_permissions(self, client: SpeckleClient): + with pytest.raises(GraphQLException): + client.workspace.get_projects_with_permissions("not a real id") + + def test_get_projects_with_permissions_method_exists(self, client: SpeckleClient): + """ + test that the method exists with the correct signature. + """ + assert hasattr(client.workspace, "get_projects_with_permissions") + method = client.workspace.get_projects_with_permissions + assert callable(method)