Gergo/workspace scoping (#5546)

* fix(workspaces): project.workspace should be behind a token scope

* fix(workspace): add limited workspace resolver to projects

* Update FE

---------

Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
This commit is contained in:
Gergő Jedlicska
2025-09-24 16:51:43 +02:00
committed by GitHub
parent 69d0b1e46e
commit 85e127f690
9 changed files with 53 additions and 16 deletions
+1
View File
@@ -1,5 +1,6 @@
[tools]
node = "22.19.0"
pre-commit = "4.3.0"
python = "3.12.11"
[env]
@@ -51,7 +51,7 @@ import { useInjectedPresentationState } from '~/lib/presentations/composables/se
import { graphql } from '~~/lib/common/generated/gql'
graphql(`
fragment PresentationLeftSidebar_Workspace on Workspace {
fragment PresentationLeftSidebar_LimitedWorkspace on LimitedWorkspace {
id
name
logo
@@ -70,7 +70,7 @@ type Documents = {
"\n fragment InviteDialogSharedSelectUsers_Workspace on Workspace {\n id\n slug\n defaultSeatType\n }\n": typeof types.InviteDialogSharedSelectUsers_WorkspaceFragmentDoc,
"\n fragment PresentationHeader_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.PresentationHeader_SavedViewGroupFragmentDoc,
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n": typeof types.PresentationLeftSidebar_WorkspaceFragmentDoc,
"\n fragment PresentationLeftSidebar_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n }\n": typeof types.PresentationLeftSidebar_LimitedWorkspaceFragmentDoc,
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": typeof types.PresentationSlideEditDialog_SavedViewFragmentDoc,
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": typeof types.PresentationSlideListSlide_SavedViewFragmentDoc,
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": typeof types.PresentationSlideList_SavedViewGroupFragmentDoc,
@@ -295,7 +295,7 @@ type Documents = {
"\n query NavigationProjectInvites {\n activeUser {\n id\n projectInvites {\n ...HeaderNavNotificationsProjectInvite_PendingStreamCollaborator\n }\n }\n }\n": typeof types.NavigationProjectInvitesDocument,
"\n query NavigationWorkspaceInvites {\n activeUser {\n id\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": typeof types.NavigationWorkspaceInvitesDocument,
"\n mutation UpdatePresentationSlide($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n name\n description\n }\n }\n }\n }\n": typeof types.UpdatePresentationSlideDocument,
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n workspace {\n id\n ...PresentationLeftSidebar_Workspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": typeof types.ProjectPresentationPageDocument,
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": typeof types.ProjectPresentationPageDocument,
"\n fragment UseCopyModelLink_Model on Model {\n id\n projectId\n ...GetModelItemRoute_Model\n }\n": typeof types.UseCopyModelLink_ModelFragmentDoc,
"\n fragment UseCanCreatePersonalProject_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCanCreatePersonalProject_UserFragmentDoc,
"\n fragment UseCanCreateWorkspace_User on User {\n permissions {\n canCreateWorkspace {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCanCreateWorkspace_UserFragmentDoc,
@@ -604,7 +604,7 @@ const documents: Documents = {
"\n fragment InviteDialogSharedSelectUsers_Workspace on Workspace {\n id\n slug\n defaultSeatType\n }\n": types.InviteDialogSharedSelectUsers_WorkspaceFragmentDoc,
"\n fragment PresentationHeader_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.PresentationHeader_SavedViewGroupFragmentDoc,
"\n fragment PresentationInfoSidebar_SavedViewGroup on SavedViewGroup {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n\n fragment PresentationInfoSidebar_SavedView on SavedView {\n id\n ...PresentationSlideEditDialog_SavedView\n name\n description\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.PresentationInfoSidebar_SavedViewGroupFragmentDoc,
"\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n": types.PresentationLeftSidebar_WorkspaceFragmentDoc,
"\n fragment PresentationLeftSidebar_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n }\n": types.PresentationLeftSidebar_LimitedWorkspaceFragmentDoc,
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": types.PresentationSlideEditDialog_SavedViewFragmentDoc,
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": types.PresentationSlideListSlide_SavedViewFragmentDoc,
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": types.PresentationSlideList_SavedViewGroupFragmentDoc,
@@ -829,7 +829,7 @@ const documents: Documents = {
"\n query NavigationProjectInvites {\n activeUser {\n id\n projectInvites {\n ...HeaderNavNotificationsProjectInvite_PendingStreamCollaborator\n }\n }\n }\n": types.NavigationProjectInvitesDocument,
"\n query NavigationWorkspaceInvites {\n activeUser {\n id\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": types.NavigationWorkspaceInvitesDocument,
"\n mutation UpdatePresentationSlide($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n name\n description\n }\n }\n }\n }\n": types.UpdatePresentationSlideDocument,
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n workspace {\n id\n ...PresentationLeftSidebar_Workspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": types.ProjectPresentationPageDocument,
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": types.ProjectPresentationPageDocument,
"\n fragment UseCopyModelLink_Model on Model {\n id\n projectId\n ...GetModelItemRoute_Model\n }\n": types.UseCopyModelLink_ModelFragmentDoc,
"\n fragment UseCanCreatePersonalProject_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCanCreatePersonalProject_UserFragmentDoc,
"\n fragment UseCanCreateWorkspace_User on User {\n permissions {\n canCreateWorkspace {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCanCreateWorkspace_UserFragmentDoc,
@@ -1323,7 +1323,7 @@ export function graphql(source: "\n fragment PresentationInfoSidebar_SavedViewG
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n"): (typeof documents)["\n fragment PresentationLeftSidebar_Workspace on Workspace {\n id\n name\n logo\n slug\n }\n"];
export function graphql(source: "\n fragment PresentationLeftSidebar_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n }\n"): (typeof documents)["\n fragment PresentationLeftSidebar_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2223,7 +2223,7 @@ export function graphql(source: "\n mutation UpdatePresentationSlide($input: Up
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n workspace {\n id\n ...PresentationLeftSidebar_Workspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n workspace {\n id\n ...PresentationLeftSidebar_Workspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"];
export function graphql(source: "\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationInfoSidebar_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@ import { useEventBus } from '~/lib/core/composables/eventBus'
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
type ResponseProject = Optional<Get<ProjectPresentationPageQuery, 'project'>>
type ResponseWorkspace = Get<ProjectPresentationPageQuery, 'project.workspace'>
type ResponseWorkspace = Get<ProjectPresentationPageQuery, 'project.limitedWorkspace'>
type ResponseGroup = Get<ResponseProject, 'savedViewGroup'>
type ResponseView = NonNullable<Get<ResponseGroup, 'views.items.0'>>
@@ -75,7 +75,7 @@ const setupStateResponse = (initState: InitState): ResponseState => {
const project = computed(() => result.value?.project)
const presentation = computed(() => project.value?.savedViewGroup)
const workspace = computed(() => project.value?.workspace)
const workspace = computed(() => project.value?.limitedWorkspace)
const slides = computed(() => presentation.value?.views.items || [])
const visibleSlides = computed(() => slides.value)
@@ -8,9 +8,9 @@ export const projectPresentationPageQuery = graphql(`
) {
project(id: $projectId) {
id
workspace {
limitedWorkspace {
id
...PresentationLeftSidebar_Workspace
...PresentationLeftSidebar_LimitedWorkspace
}
savedViewGroup(id: $savedViewGroupId) {
id
@@ -563,7 +563,14 @@ extend type User {
}
extend type Project {
workspace: Workspace
"""
Full workspace information for the project.
"""
workspace: Workspace @hasScope(scope: "workspace:read")
"""
Limited workspace records that exposes public data projects workspaces.
"""
limitedWorkspace: LimitedWorkspace
"""
Public project-level configuration for embedded viewer
"""
@@ -2547,6 +2547,8 @@ export type Project = {
invitableCollaborators: WorkspaceCollaboratorCollection;
/** Collaborators who have been invited, but not yet accepted. */
invitedTeam?: Maybe<Array<PendingStreamCollaborator>>;
/** Limited workspace records that exposes public data projects workspaces. */
limitedWorkspace?: Maybe<LimitedWorkspace>;
/** Returns a specific model by its ID */
model: Model;
/** Retrieve a specific project model by its ID */
@@ -2594,6 +2596,7 @@ export type Project = {
viewerResourcesExtended: ExtendedViewerResources;
visibility: ProjectVisibility;
webhooks: WebhookCollection;
/** Full workspace information for the project. */
workspace?: Maybe<Workspace>;
workspaceId?: Maybe<Scalars['String']['output']>;
};
@@ -7909,6 +7912,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
invitableCollaborators?: Resolver<ResolversTypes['WorkspaceCollaboratorCollection'], ParentType, ContextType, RequireFields<ProjectInvitableCollaboratorsArgs, 'limit'>>;
invitedTeam?: Resolver<Maybe<Array<ResolversTypes['PendingStreamCollaborator']>>, ParentType, ContextType>;
limitedWorkspace?: Resolver<Maybe<ResolversTypes['LimitedWorkspace']>, ParentType, ContextType>;
model?: Resolver<ResolversTypes['Model'], ParentType, ContextType, RequireFields<ProjectModelArgs, 'id'>>;
modelByName?: Resolver<ResolversTypes['Model'], ParentType, ContextType, RequireFields<ProjectModelByNameArgs, 'name'>>;
modelChildrenTree?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType, RequireFields<ProjectModelChildrenTreeArgs, 'fullName'>>;
@@ -2077,6 +2077,27 @@ export default FF_WORKSPACES_MODULE_ENABLED
}
},
Project: {
limitedWorkspace: async (parent, _args, context) => {
if (!parent.workspaceId) {
return null
}
const workspace = await context.loaders.workspaces!.getWorkspace.load(
parent.workspaceId
)
if (!workspace) {
throw new WorkspaceNotFoundError()
}
await authorizeResolver(
context.userId,
parent.workspaceId,
Roles.Workspace.Guest,
context.resourceAccessRules
)
return workspace
},
workspace: async (parent, _args, context) => {
if (!parent.workspaceId) {
return null