feat: tightening up saved views permissions (#5239)
* updated auth policies * added auth checks to resolvers * tests for single view resolvers
This commit is contained in:
committed by
GitHub
parent
a7bebb0882
commit
d013fe1dd7
@@ -9134,6 +9134,14 @@ export type GetProjectSavedViewQueryVariables = Exact<{
|
||||
|
||||
export type GetProjectSavedViewQuery = { __typename?: 'Query', project: { __typename?: 'Project', savedView: { __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } } } };
|
||||
|
||||
export type GetProjectSavedViewIfExistsQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
viewId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectSavedViewIfExistsQuery = { __typename?: 'Query', project: { __typename?: 'Project', savedViewIfExists?: { __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } } | null } };
|
||||
|
||||
export type DeleteSavedViewMutationVariables = Exact<{
|
||||
input: DeleteSavedViewInput;
|
||||
}>;
|
||||
@@ -10313,6 +10321,7 @@ export const GetProjectSavedViewGroupsDocument = {"kind":"Document","definitions
|
||||
export const GetProjectSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"groupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedViewGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"groupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectSavedViewGroupQuery, GetProjectSavedViewGroupQueryVariables>;
|
||||
export const GetProjectUngroupedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectUngroupedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetUngroupedViewGroupInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ungroupedViewGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectUngroupedViewGroupQuery, GetProjectUngroupedViewGroupQueryVariables>;
|
||||
export const GetProjectSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<GetProjectSavedViewQuery, GetProjectSavedViewQueryVariables>;
|
||||
export const GetProjectSavedViewIfExistsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedViewIfExists"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedViewIfExists"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<GetProjectSavedViewIfExistsQuery, GetProjectSavedViewIfExistsQueryVariables>;
|
||||
export const DeleteSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteSavedViewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedViewMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<DeleteSavedViewMutation, DeleteSavedViewMutationVariables>;
|
||||
export const CanCreateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CanCreateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateSavedView"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CanCreateSavedViewQuery, CanCreateSavedViewQueryVariables>;
|
||||
export const CanUpdateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CanUpdateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canUpdate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<CanUpdateSavedViewQuery, CanUpdateSavedViewQueryVariables>;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.WorkspacePlanNoFeatureAccessError.code:
|
||||
case Authz.EligibleForExclusiveWorkspaceError.code:
|
||||
case Authz.AutomateFunctionNotCreatorError.code:
|
||||
case Authz.SavedViewNoAccessError.code:
|
||||
return new ForbiddenError(e.message)
|
||||
case Authz.WorkspaceSsoSessionNoAccessError.code:
|
||||
throw new SsoSessionMissingOrExpiredError(e.message, {
|
||||
|
||||
@@ -40,7 +40,7 @@ const resolvers: Resolvers = {
|
||||
const canUpdate = await ctx.authPolicies.project.savedViews.canUpdateGroup({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.savedViewGroup.projectId,
|
||||
groupId: savedViewGroupId
|
||||
savedViewGroupId
|
||||
})
|
||||
return toGraphqlResult(canUpdate)
|
||||
}
|
||||
|
||||
@@ -122,12 +122,20 @@ const resolvers: Resolvers = {
|
||||
return group
|
||||
},
|
||||
savedView: async (parent, args, ctx) => {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
const projectId = parent.id
|
||||
const canRead = await ctx.authPolicies.project.savedViews.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
savedViewId: args.id
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const view = await ctx.loaders
|
||||
.forRegion({ db: projectDb })
|
||||
.savedViews.getSavedView.load({
|
||||
viewId: args.id,
|
||||
projectId: parent.id
|
||||
projectId
|
||||
})
|
||||
if (!view) {
|
||||
throw new NotFoundError(
|
||||
@@ -138,16 +146,27 @@ const resolvers: Resolvers = {
|
||||
return view
|
||||
},
|
||||
savedViewIfExists: async (parent, args, ctx) => {
|
||||
const projectId = parent.id
|
||||
if (!args.id?.length) return null
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const view = await ctx.loaders
|
||||
.forRegion({ db: projectDb })
|
||||
.savedViews.getSavedView.load({
|
||||
viewId: args.id,
|
||||
projectId: parent.id
|
||||
projectId
|
||||
})
|
||||
|
||||
if (view) {
|
||||
// Only access check if found
|
||||
const canRead = await ctx.authPolicies.project.savedViews.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
savedViewId: args.id
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
},
|
||||
@@ -385,7 +404,7 @@ const resolvers: Resolvers = {
|
||||
const canDelete = await ctx.authPolicies.project.savedViews.canUpdateGroup({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
groupId: args.input.groupId
|
||||
savedViewGroupId: args.input.groupId
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
@@ -417,7 +436,7 @@ const resolvers: Resolvers = {
|
||||
const canUpdate = await ctx.authPolicies.project.savedViews.canUpdateGroup({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
groupId: args.input.groupId
|
||||
savedViewGroupId: args.input.groupId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { getStreamObjectsFactory } from '@/modules/core/repositories/objects'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { getSavedViewFactory } from '@/modules/viewer/repositories/dataLoaders/savedViews'
|
||||
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
|
||||
|
||||
@@ -20,7 +21,19 @@ const resolvers: Resolvers = {
|
||||
{ resourceIdString, loadedVersionsOnly, savedViewId, savedViewSettings },
|
||||
ctx
|
||||
) {
|
||||
const projectDB = await getProjectDbClient({ projectId: parent.id })
|
||||
const projectId = parent.id
|
||||
const projectDB = await getProjectDbClient({ projectId })
|
||||
|
||||
// If savedViewId set, check for access
|
||||
if (savedViewId) {
|
||||
const canRead = await ctx.authPolicies.project.savedViews.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
savedViewId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
}
|
||||
|
||||
const getStreamObjects = getStreamObjectsFactory({ db: projectDB })
|
||||
const getViewerResourceGroups = getViewerResourceGroupsFactory({
|
||||
getStreamObjects,
|
||||
|
||||
@@ -134,6 +134,17 @@ export const getProjectSavedViewQuery = gql`
|
||||
${basicSavedViewFragment}
|
||||
`
|
||||
|
||||
export const getProjectSavedViewIfExistsQuery = gql`
|
||||
query GetProjectSavedViewIfExists($projectId: String!, $viewId: ID!) {
|
||||
project(id: $projectId) {
|
||||
savedViewIfExists(id: $viewId) {
|
||||
...BasicSavedView
|
||||
}
|
||||
}
|
||||
}
|
||||
${basicSavedViewFragment}
|
||||
`
|
||||
|
||||
export const deleteSavedViewMutation = gql`
|
||||
mutation DeleteSavedView($input: DeleteSavedViewInput!) {
|
||||
projectMutations {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
DeleteSavedViewMutationVariables,
|
||||
GetProjectSavedViewGroupQueryVariables,
|
||||
GetProjectSavedViewGroupsQueryVariables,
|
||||
GetProjectSavedViewIfExistsQueryVariables,
|
||||
GetProjectSavedViewQueryVariables,
|
||||
GetProjectUngroupedViewGroupQueryVariables,
|
||||
UpdateSavedViewGroupMutationVariables,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
GetProjectSavedViewDocument,
|
||||
GetProjectSavedViewGroupDocument,
|
||||
GetProjectSavedViewGroupsDocument,
|
||||
GetProjectSavedViewIfExistsDocument,
|
||||
GetProjectUngroupedViewGroupDocument,
|
||||
UpdateSavedViewDocument,
|
||||
UpdateSavedViewGroupDocument
|
||||
@@ -64,6 +66,7 @@ import { addToStream, createTestStream } from '@/test/speckle-helpers/streamHelp
|
||||
import { Roles, WorkspacePlans } from '@speckle/shared'
|
||||
import {
|
||||
ProjectNotEnoughPermissionsError,
|
||||
SavedViewNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '@speckle/shared/authz'
|
||||
import * as ViewerRoute from '@speckle/shared/viewer/route'
|
||||
@@ -168,6 +171,11 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectSavedViewDocument, input, options)
|
||||
|
||||
const getViewIfExists = (
|
||||
input: GetProjectSavedViewIfExistsQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectSavedViewIfExistsDocument, input, options)
|
||||
|
||||
const deleteView = (
|
||||
input: DeleteSavedViewMutationVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
@@ -1414,7 +1422,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
|
||||
const data = res.data?.project.savedView.permissions.canUpdate
|
||||
expect(data?.authorized).to.be.false
|
||||
expect(data?.code).to.equal(ProjectNotEnoughPermissionsError.code)
|
||||
expect(data?.code).to.equal(SavedViewNoAccessError.code)
|
||||
})
|
||||
|
||||
describe('of groups', async () => {
|
||||
@@ -1502,6 +1510,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
const modelIds: string[] = []
|
||||
let readTestProject: BasicTestStream
|
||||
let otherReader: BasicTestUser
|
||||
let otherReaderAdmin: BasicTestUser
|
||||
|
||||
const getAllReadModelResourceIds = () =>
|
||||
ViewerRoute.resourceBuilder().addResources(
|
||||
@@ -1509,13 +1518,27 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
)
|
||||
|
||||
before(async () => {
|
||||
otherReader = await createTestUser(buildBasicTestUser({ name: 'other-reader' }))
|
||||
await assignToWorkspace(
|
||||
myProjectWorkspace,
|
||||
otherReader,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Editor
|
||||
)
|
||||
const otherReaders = await Promise.all([
|
||||
createTestUser(buildBasicTestUser({ name: 'other-reader' })),
|
||||
createTestUser(buildBasicTestUser({ name: 'other-reader-admin' }))
|
||||
])
|
||||
otherReader = otherReaders[0]
|
||||
otherReaderAdmin = otherReaders[1]
|
||||
|
||||
await Promise.all([
|
||||
assignToWorkspace(
|
||||
myProjectWorkspace,
|
||||
otherReader,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Editor
|
||||
),
|
||||
assignToWorkspace(
|
||||
myProjectWorkspace,
|
||||
otherReaderAdmin,
|
||||
Roles.Workspace.Admin,
|
||||
WorkspaceSeatType.Editor
|
||||
)
|
||||
])
|
||||
|
||||
readTestProject = await createTestStream(
|
||||
buildBasicTestProject({
|
||||
@@ -1525,9 +1548,14 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
me
|
||||
)
|
||||
|
||||
await addToStream(readTestProject, otherReader, Roles.Stream.Contributor, {
|
||||
owner: me
|
||||
})
|
||||
await Promise.all([
|
||||
addToStream(readTestProject, otherReader, Roles.Stream.Contributor, {
|
||||
owner: me
|
||||
}),
|
||||
addToStream(readTestProject, otherReaderAdmin, Roles.Stream.Owner, {
|
||||
owner: me
|
||||
})
|
||||
])
|
||||
|
||||
// Create a bunch of groups (views w/ groupNames), each w/ a different model
|
||||
let includedSearchString = 0
|
||||
@@ -1911,27 +1939,80 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully read specific view', async () => {
|
||||
const view = myFirstGroupViews[0]
|
||||
it('should fail to read private view, even as workspace admin/project owner', async () => {
|
||||
const view = myFirstGroupViews.find(
|
||||
(v) =>
|
||||
v.author?.id !== otherReaderAdmin.id &&
|
||||
v.visibility === SavedViewVisibility.authorOnly
|
||||
)
|
||||
expect(view).to.be.ok
|
||||
|
||||
const res = await getView(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
viewId: view.id
|
||||
viewId: view!.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
{ authUserId: otherReaderAdmin.id }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedView
|
||||
expect(data).to.be.ok
|
||||
expect(data!.id).to.equal(view.id)
|
||||
expect(data!.name).to.equal(view.name)
|
||||
expect(data!.description).to.equal(view.description)
|
||||
expect(data!.author?.id).to.equal(view.author?.id)
|
||||
expect(data!.groupId).to.equal(view.groupId)
|
||||
expect(data!.createdAt.toISOString()).to.equal(view.createdAt.toISOString())
|
||||
expect(data!.group.id).to.equal(myFirstGroup.id)
|
||||
expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code })
|
||||
expect(res.data?.project.savedView).to.not.be.ok
|
||||
|
||||
const res2 = await getViewIfExists(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
viewId: view!.id
|
||||
},
|
||||
{ authUserId: otherReaderAdmin.id }
|
||||
)
|
||||
|
||||
expect(res2).to.haveGraphQLErrors({ code: ForbiddenError.code })
|
||||
expect(res2.data?.project.savedViewIfExists).to.not.be.ok
|
||||
})
|
||||
|
||||
itEach(
|
||||
[{ savedViewIfExists: false }, { savedViewIfExists: true }],
|
||||
({ savedViewIfExists }) =>
|
||||
`should successfully read specific view (w/ ${
|
||||
savedViewIfExists ? 'savedViewIfExists' : 'savedView '
|
||||
})`,
|
||||
async ({ savedViewIfExists }) => {
|
||||
const view = myFirstGroupViews.find((v) => v.author?.id === me.id)!
|
||||
|
||||
let data: BasicSavedViewFragment | undefined = undefined
|
||||
if (savedViewIfExists) {
|
||||
const res = await getViewIfExists(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
viewId: view.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
data = res.data?.project.savedViewIfExists || undefined
|
||||
} else {
|
||||
const res = await getView(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
viewId: view.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
data = res.data?.project.savedView
|
||||
}
|
||||
|
||||
expect(data).to.be.ok
|
||||
expect(data!.id).to.equal(view.id)
|
||||
expect(data!.name).to.equal(view.name)
|
||||
expect(data!.description).to.equal(view.description)
|
||||
expect(data!.author?.id).to.equal(view.author?.id)
|
||||
expect(data!.groupId).to.equal(view.groupId)
|
||||
expect(data!.createdAt.toISOString()).to.equal(view.createdAt.toISOString())
|
||||
expect(data!.group.id).to.equal(myFirstGroup.id)
|
||||
}
|
||||
)
|
||||
|
||||
it('should get NotFoundError if trying to get nonexistant view', async () => {
|
||||
const res = await getView({
|
||||
projectId: readTestProject.id,
|
||||
@@ -1942,6 +2023,16 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
expect(res.data?.project.savedView).to.not.be.ok
|
||||
})
|
||||
|
||||
it('should not get errors if trying to get nonexistant view through savedViewIfExists', async () => {
|
||||
const res = await getViewIfExists({
|
||||
projectId: readTestProject.id,
|
||||
viewId: 'zabababababababa'
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.project.savedViewIfExists).to.eq(null)
|
||||
})
|
||||
|
||||
it('should successfully read a group with its views', async () => {
|
||||
const res = await getGroup(
|
||||
{
|
||||
|
||||
@@ -206,6 +206,11 @@ export const SavedViewNotFoundError = defineAuthError({
|
||||
message: 'Saved view not found'
|
||||
})
|
||||
|
||||
export const SavedViewNoAccessError = defineAuthError({
|
||||
code: 'SavedViewNoAccess',
|
||||
message: 'You do not have access to this saved view'
|
||||
})
|
||||
|
||||
export const SavedViewGroupNotFoundError = defineAuthError({
|
||||
code: 'SavedViewGroupNotFound',
|
||||
message: 'Saved view group not found'
|
||||
|
||||
@@ -14,3 +14,7 @@ export type ModelContext = { modelId: string }
|
||||
export type VersionContext = { versionId: string }
|
||||
|
||||
export type AutomateFunctionContext = { functionId: string }
|
||||
|
||||
export type SavedViewContext = { savedViewId: string }
|
||||
|
||||
export type SavedViewGroupContext = { savedViewGroupId: string }
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { StringEnum, StringEnumValues } from '../../../core/helpers/utility.js'
|
||||
|
||||
export const SavedViewVisibility = StringEnum(['public', 'authorOnly'])
|
||||
export type SavedViewVisibility = StringEnumValues<typeof SavedViewVisibility>
|
||||
|
||||
export type SavedView = {
|
||||
id: string
|
||||
name: string
|
||||
authorId: string | null
|
||||
groupId: string | null
|
||||
projectId: string
|
||||
visibility: SavedViewVisibility
|
||||
}
|
||||
|
||||
export type SavedViewGroup = {
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../tests/helpers/types.js'
|
||||
import {
|
||||
ensureCanAccessSavedViewFragment,
|
||||
ensureCanAccessSavedViewGroupFragment
|
||||
} from './savedViews.js'
|
||||
import {
|
||||
getEnvFake,
|
||||
getProjectFake,
|
||||
getSavedViewFake,
|
||||
getSavedViewGroupFake,
|
||||
getWorkspaceFake,
|
||||
getWorkspacePlanFake
|
||||
} from '../../tests/fakes.js'
|
||||
import { SavedViewVisibility } from '../domain/savedViews/types.js'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
import {
|
||||
ProjectNotEnoughPermissionsError,
|
||||
SavedViewGroupNotFoundError,
|
||||
SavedViewNoAccessError,
|
||||
SavedViewNotFoundError,
|
||||
UngroupedSavedViewGroupLockError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
const userId = 'user-id'
|
||||
const savedViewId = 'saved-view-id'
|
||||
const projectId = 'project-id'
|
||||
const workspaceId = 'workspace-id'
|
||||
const savedViewGroupId = 'saved-view-group-id'
|
||||
|
||||
describe('ensureCanAccessSavedViewFragment', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof ensureCanAccessSavedViewFragment>) =>
|
||||
ensureCanAccessSavedViewFragment({
|
||||
getSavedView: getSavedViewFake({
|
||||
id: savedViewId,
|
||||
projectId,
|
||||
visibility: SavedViewVisibility.public
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: projectId,
|
||||
workspaceId: null
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: getEnvFake({
|
||||
FF_WORKSPACES_MODULE_ENABLED: true,
|
||||
FF_SAVED_VIEWS_ENABLED: true
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when not workspaced project (%s)',
|
||||
async (access) => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe('w/ workspaced project', () => {
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof ensureCanAccessSavedViewFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: projectId,
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspace: getWorkspaceFake({
|
||||
id: workspaceId
|
||||
}),
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'pro'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it.each(<const>[
|
||||
{ author: 'author', success: 'succeeds' },
|
||||
{ author: 'not author', success: 'succeeds' },
|
||||
{
|
||||
author: 'not author and view is private',
|
||||
success: 'fails',
|
||||
error: SavedViewNoAccessError.code
|
||||
}
|
||||
])(
|
||||
'$success if asking for read access (as $author)',
|
||||
async ({ author, success, error }) => {
|
||||
const isAuthor = author.startsWith('author')
|
||||
const isPrivate = author.includes('view is private')
|
||||
const expectSuccess = success === 'succeeds'
|
||||
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getSavedView: getSavedViewFake({
|
||||
id: savedViewId,
|
||||
projectId,
|
||||
visibility: isPrivate
|
||||
? SavedViewVisibility.authorOnly
|
||||
: SavedViewVisibility.public,
|
||||
authorId: isAuthor ? userId : nanoid()
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access: 'read'
|
||||
})
|
||||
|
||||
if (expectSuccess) {
|
||||
expect(result).toBeAuthOKResult()
|
||||
} else {
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: error
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each(<const>[
|
||||
{ author: 'author', success: 'succeeds' },
|
||||
{ author: 'not author', success: 'fails', error: SavedViewNoAccessError.code },
|
||||
{
|
||||
author: 'author but no longer contributor',
|
||||
success: 'fails',
|
||||
error: ProjectNotEnoughPermissionsError.code
|
||||
},
|
||||
{
|
||||
author: 'not author but is workspace admin',
|
||||
success: 'fails',
|
||||
error: SavedViewNoAccessError.code
|
||||
}
|
||||
])(
|
||||
'$success if asking for write access to private (as $author)',
|
||||
async ({ author, success, error }) => {
|
||||
const isAuthor = author.startsWith('author')
|
||||
const isNotContributor = author.includes('no longer contributor')
|
||||
const isWorkspaceAdmin = author.includes('is workspace admin')
|
||||
const expectSuccess = success === 'succeeds'
|
||||
|
||||
const sut = buildWorkspaceSUT({
|
||||
getSavedView: getSavedViewFake({
|
||||
id: savedViewId,
|
||||
projectId,
|
||||
visibility: SavedViewVisibility.public,
|
||||
authorId: isAuthor ? userId : nanoid()
|
||||
}),
|
||||
getProjectRole: async () =>
|
||||
isNotContributor ? Roles.Stream.Reviewer : Roles.Stream.Contributor,
|
||||
getWorkspaceRole: async () =>
|
||||
isWorkspaceAdmin ? Roles.Workspace.Admin : Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access: 'write'
|
||||
})
|
||||
|
||||
if (expectSuccess) {
|
||||
expect(result).toBeAuthOKResult()
|
||||
} else {
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: error
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when workspace plan is too cheap (%s)',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'team'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails if view doesnt exist (%s)',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getSavedView: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: SavedViewNotFoundError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureCanAccessSavedViewGroupFragment', () => {
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof ensureCanAccessSavedViewGroupFragment>
|
||||
) =>
|
||||
ensureCanAccessSavedViewGroupFragment({
|
||||
getSavedViewGroup: getSavedViewGroupFake({
|
||||
projectId,
|
||||
id: savedViewGroupId
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: projectId,
|
||||
workspaceId: null
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: getEnvFake({
|
||||
FF_WORKSPACES_MODULE_ENABLED: true,
|
||||
FF_SAVED_VIEWS_ENABLED: true
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when not workspaced project (%s)',
|
||||
async (access) => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe('w/ workspaced project', () => {
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof ensureCanAccessSavedViewGroupFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: projectId,
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspace: getWorkspaceFake({
|
||||
id: workspaceId
|
||||
}),
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'pro'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it.each(<const>[
|
||||
{ author: 'author', success: 'succeeds' },
|
||||
{ author: 'not author', success: 'succeeds' }
|
||||
])('$success if asking for read access (as $author)', async ({ author }) => {
|
||||
const isAuthor = author.startsWith('author')
|
||||
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getSavedViewGroup: getSavedViewGroupFake({
|
||||
id: savedViewGroupId,
|
||||
projectId,
|
||||
authorId: isAuthor ? userId : nanoid()
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access: 'read'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it.each(<const>[
|
||||
{ author: 'author', success: 'succeeds' },
|
||||
{
|
||||
author: 'not author',
|
||||
success: 'fails',
|
||||
error: ProjectNotEnoughPermissionsError.code
|
||||
},
|
||||
{
|
||||
author: 'author but no longer contributor',
|
||||
success: 'fails',
|
||||
error: ProjectNotEnoughPermissionsError.code
|
||||
},
|
||||
{
|
||||
author: 'not author but is workspace admin',
|
||||
success: 'succeeds'
|
||||
}
|
||||
])(
|
||||
'$success if asking for write access to private (as $author)',
|
||||
async ({ author, success, error }) => {
|
||||
const isAuthor = author.startsWith('author')
|
||||
const isNotContributor = author.includes('no longer contributor')
|
||||
const isWorkspaceAdmin = author.includes('is workspace admin')
|
||||
const expectSuccess = success === 'succeeds'
|
||||
|
||||
const sut = buildWorkspaceSUT({
|
||||
getSavedViewGroup: getSavedViewGroupFake({
|
||||
id: savedViewId,
|
||||
projectId,
|
||||
authorId: isAuthor ? userId : nanoid()
|
||||
}),
|
||||
getProjectRole: async () =>
|
||||
isNotContributor ? Roles.Stream.Reviewer : Roles.Stream.Contributor,
|
||||
getWorkspaceRole: async () =>
|
||||
isWorkspaceAdmin ? Roles.Workspace.Admin : Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access: 'write'
|
||||
})
|
||||
|
||||
if (expectSuccess) {
|
||||
expect(result).toBeAuthOKResult()
|
||||
} else {
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: error
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('fails if writing to default group', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getSavedViewGroup: getSavedViewGroupFake({
|
||||
projectId: 'project-id',
|
||||
id: 'default-XXX'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
savedViewGroupId: 'default-XXX',
|
||||
access: 'write'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: UngroupedSavedViewGroupLockError.code
|
||||
})
|
||||
})
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when workspace plan is too cheap (%s)',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'team'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails if view doesnt exist (%s)',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getSavedViewGroup: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: SavedViewGroupNotFoundError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,208 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
SavedViewGroupNotFoundError,
|
||||
SavedViewNoAccessError,
|
||||
SavedViewNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
UngroupedSavedViewGroupLockError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError,
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
SavedViewContext,
|
||||
SavedViewGroupContext
|
||||
} from '../domain/context.js'
|
||||
import { Loaders } from '../domain/loaders.js'
|
||||
import { AuthPolicyEnsureFragment } from '../domain/policies.js'
|
||||
import { SavedViewVisibility } from '../domain/savedViews/types.js'
|
||||
import {
|
||||
ensureCanUseProjectWorkspacePlanFeatureFragment,
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment
|
||||
} from './projects.js'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
import { WorkspacePlanFeatures } from '../../workspaces/index.js'
|
||||
import { isUngroupedGroup } from '../../saved-views/index.js'
|
||||
|
||||
/**
|
||||
* Ensure the user can access the view
|
||||
*/
|
||||
export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getSavedView
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext &
|
||||
SavedViewContext & {
|
||||
access: 'read' | 'write'
|
||||
},
|
||||
InstanceType<
|
||||
| typeof SavedViewNotFoundError
|
||||
| typeof SavedViewNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacesNotEnabledError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, savedViewId, access }) => {
|
||||
const canUseSavedViews = await ensureCanUseProjectWorkspacePlanFeatureFragment(
|
||||
loaders
|
||||
)({
|
||||
projectId,
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
})
|
||||
if (canUseSavedViews.isErr) return err(canUseSavedViews.error)
|
||||
|
||||
const savedView = await loaders.getSavedView({ projectId, savedViewId })
|
||||
if (!savedView) return err(new SavedViewNotFoundError())
|
||||
|
||||
const isPublic = savedView.visibility === SavedViewVisibility.public
|
||||
if (isPublic && access === 'read') {
|
||||
return ok()
|
||||
}
|
||||
|
||||
const isAuthor = savedView.authorId === userId
|
||||
if (isAuthor) {
|
||||
if (access === 'write') {
|
||||
// Check for write access to project first
|
||||
const ensuredWriteAccess =
|
||||
await ensureImplicitProjectMemberWithWriteAccessFragment(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved views."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
}
|
||||
return ok()
|
||||
}
|
||||
|
||||
return err(
|
||||
new SavedViewNoAccessError({
|
||||
message:
|
||||
access === 'write'
|
||||
? 'You do not have write access for this saved view'
|
||||
: 'You do not have read access for this saved view'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the user can access the view group
|
||||
*/
|
||||
export const ensureCanAccessSavedViewGroupFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getSavedViewGroup
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext &
|
||||
SavedViewGroupContext & {
|
||||
access: 'read' | 'write'
|
||||
},
|
||||
InstanceType<
|
||||
| typeof SavedViewGroupNotFoundError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacesNotEnabledError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof UngroupedSavedViewGroupLockError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, savedViewGroupId, access }) => {
|
||||
const canUseSavedViews = await ensureCanUseProjectWorkspacePlanFeatureFragment(
|
||||
loaders
|
||||
)({
|
||||
projectId,
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
})
|
||||
if (canUseSavedViews.isErr) return err(canUseSavedViews.error)
|
||||
|
||||
const savedViewGroup = await loaders.getSavedViewGroup({
|
||||
projectId,
|
||||
groupId: savedViewGroupId
|
||||
})
|
||||
if (!savedViewGroup) return err(new SavedViewGroupNotFoundError())
|
||||
|
||||
if (access === 'read') {
|
||||
return ok() // read access available to everyone who has access to project
|
||||
}
|
||||
|
||||
// Prevent default group updates (as it doesnt exist)
|
||||
if (isUngroupedGroup(savedViewGroup.id)) {
|
||||
return err(new UngroupedSavedViewGroupLockError())
|
||||
}
|
||||
|
||||
// groups have no visibility (yet), so authors AND project owners can mutate
|
||||
const isAuthor = savedViewGroup.authorId === userId
|
||||
const expectedProjectRole = isAuthor ? Roles.Stream.Contributor : Roles.Stream.Owner
|
||||
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: expectedProjectRole
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved view groups."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import { canReadAccIntegrationSettingsPolicy } from './project/canReadAccIntegra
|
||||
import { canCreateSavedViewPolicy } from './project/savedViews/canCreate.js'
|
||||
import { canUpdateSavedViewPolicy } from './project/savedViews/canUpdate.js'
|
||||
import { canUpdateSavedViewGroupPolicy } from './project/savedViews/canUpdateGroup.js'
|
||||
import { canReadSavedViewPolicy } from './project/savedViews/canRead.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
automate: {
|
||||
@@ -70,7 +71,8 @@ export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
savedViews: {
|
||||
canCreate: canCreateSavedViewPolicy(loaders),
|
||||
canUpdate: canUpdateSavedViewPolicy(loaders),
|
||||
canUpdateGroup: canUpdateSavedViewGroupPolicy(loaders)
|
||||
canUpdateGroup: canUpdateSavedViewGroupPolicy(loaders),
|
||||
canRead: canReadSavedViewPolicy(loaders)
|
||||
},
|
||||
canBroadcastActivity: canBroadcastProjectActivityPolicy(loaders),
|
||||
canRead: canReadProjectPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
SavedViewNoAccessError,
|
||||
SavedViewNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError,
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
SavedViewContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { ensureCanAccessSavedViewFragment } from '../../../fragments/savedViews.js'
|
||||
|
||||
export const canReadSavedViewPolicy: AuthPolicy<
|
||||
| typeof Loaders.getSavedView
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & ProjectContext & SavedViewContext,
|
||||
InstanceType<
|
||||
| typeof SavedViewNotFoundError
|
||||
| typeof SavedViewNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacesNotEnabledError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, savedViewId }) => {
|
||||
return await ensureCanAccessSavedViewFragment(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewId,
|
||||
access: 'read'
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
ProjectNotEnoughPermissionsError,
|
||||
SavedViewNoAccessError,
|
||||
ServerNoAccessError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError,
|
||||
@@ -23,7 +24,8 @@ describe('canUpdateSavedViewPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canUpdateSavedViewPolicy>) =>
|
||||
canUpdateSavedViewPolicy({
|
||||
getSavedView: getSavedViewFake({
|
||||
projectId: 'project-id'
|
||||
projectId: 'project-id',
|
||||
authorId: 'user-id'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
@@ -83,16 +85,18 @@ describe('canUpdateSavedViewPolicy', () => {
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('works if user is project owner', async () => {
|
||||
const sut = buildWorkspacedSUT()
|
||||
it('doesnt work for non-author even if user is project owner', async () => {
|
||||
const sut = buildWorkspacedSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
userId: 'user-idx',
|
||||
projectId: 'project-id',
|
||||
savedViewId: 'saved-view-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
expect(result).toBeAuthErrorResult({ code: SavedViewNoAccessError.code })
|
||||
})
|
||||
|
||||
it('fails if workspaces disabled', async () => {
|
||||
@@ -157,7 +161,7 @@ describe('canUpdateSavedViewPolicy', () => {
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'aaa',
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
savedViewId: 'saved-view-id'
|
||||
})
|
||||
@@ -166,33 +170,12 @@ describe('canUpdateSavedViewPolicy', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if not owner and not the author', async () => {
|
||||
const sut = buildWorkspacedSUT({
|
||||
getSavedView: getSavedViewFake({
|
||||
projectId: 'project-id',
|
||||
authorId: 'another-user-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
savedViewId: 'saved-view-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotEnoughPermissionsError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds if not owner but author', async () => {
|
||||
it('succeeds if view author', async () => {
|
||||
const sut = buildWorkspacedSUT({
|
||||
getSavedView: getSavedViewFake({
|
||||
projectId: 'project-id',
|
||||
authorId: 'user-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { WorkspacePlanFeatures } from '../../../../workspaces/index.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ProjectNotFoundError,
|
||||
SavedViewNoAccessError,
|
||||
SavedViewNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
@@ -15,14 +14,14 @@ import {
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
SavedViewContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
ensureCanUseProjectWorkspacePlanFeatureFragment,
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment
|
||||
} from '../../../fragments/projects.js'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { ensureCanAccessSavedViewFragment } from '../../../fragments/savedViews.js'
|
||||
|
||||
export const canUpdateSavedViewPolicy: AuthPolicy<
|
||||
| typeof Loaders.getSavedView
|
||||
@@ -31,15 +30,14 @@ export const canUpdateSavedViewPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext & {
|
||||
savedViewId: string
|
||||
},
|
||||
MaybeUserContext & ProjectContext & SavedViewContext,
|
||||
InstanceType<
|
||||
| typeof SavedViewNotFoundError
|
||||
| typeof SavedViewNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
@@ -49,64 +47,17 @@ export const canUpdateSavedViewPolicy: AuthPolicy<
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacesNotEnabledError
|
||||
| typeof SavedViewNotFoundError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, savedViewId }) => {
|
||||
const canUseSavedViews = await ensureCanUseProjectWorkspacePlanFeatureFragment(
|
||||
loaders
|
||||
)({
|
||||
projectId,
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
})
|
||||
if (canUseSavedViews.isErr) return err(canUseSavedViews.error)
|
||||
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
return await ensureCanAccessSavedViewFragment(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Contributor
|
||||
savedViewId,
|
||||
access: 'write'
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved views. You need to be the author of the view or the Project owner."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
// Even if user has access to project - must be author OR project admin
|
||||
const savedView = await loaders.getSavedView({
|
||||
projectId,
|
||||
savedViewId
|
||||
})
|
||||
if (!savedView) return err(new SavedViewNotFoundError())
|
||||
|
||||
if (savedView.authorId !== userId) {
|
||||
const ensuredWriteAccess =
|
||||
await ensureImplicitProjectMemberWithWriteAccessFragment(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message: "Only project owners can update saved views they don't own."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await policy({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -90,7 +90,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
@@ -107,7 +107,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -126,7 +126,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -142,7 +142,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -160,7 +160,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'aaa',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
@@ -179,7 +179,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -198,7 +198,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'default-XXX'
|
||||
savedViewGroupId: 'default-XXX'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -218,7 +218,7 @@ describe('canUpdateSavedViewGroupPolicy', () => {
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
groupId: 'saved-group-id'
|
||||
savedViewGroupId: 'saved-group-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { isUngroupedGroup } from '../../../../saved-views/index.js'
|
||||
import { WorkspacePlanFeatures } from '../../../../workspaces/index.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
@@ -17,14 +14,14 @@ import {
|
||||
WorkspacesNotEnabledError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import {
|
||||
MaybeUserContext,
|
||||
ProjectContext,
|
||||
SavedViewGroupContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
ensureCanUseProjectWorkspacePlanFeatureFragment,
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment
|
||||
} from '../../../fragments/projects.js'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { ensureCanAccessSavedViewGroupFragment } from '../../../fragments/savedViews.js'
|
||||
|
||||
export const canUpdateSavedViewGroupPolicy: AuthPolicy<
|
||||
| typeof Loaders.getSavedViewGroup
|
||||
@@ -33,15 +30,13 @@ export const canUpdateSavedViewGroupPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspacePlan
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext & {
|
||||
groupId: string
|
||||
},
|
||||
MaybeUserContext & ProjectContext & SavedViewGroupContext,
|
||||
InstanceType<
|
||||
| typeof SavedViewGroupNotFoundError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
@@ -51,71 +46,18 @@ export const canUpdateSavedViewGroupPolicy: AuthPolicy<
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacesNotEnabledError
|
||||
| typeof SavedViewGroupNotFoundError
|
||||
| typeof WorkspaceReadOnlyError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof UngroupedSavedViewGroupLockError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, groupId }) => {
|
||||
const canUseSavedViews = await ensureCanUseProjectWorkspacePlanFeatureFragment(
|
||||
loaders
|
||||
)({
|
||||
projectId,
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
})
|
||||
if (canUseSavedViews.isErr) return err(canUseSavedViews.error)
|
||||
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
async ({ userId, projectId, savedViewGroupId }) => {
|
||||
return await ensureCanAccessSavedViewGroupFragment(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Contributor
|
||||
savedViewGroupId,
|
||||
access: 'write'
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved views. You need to be the author of the view or the Project owner."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
// Even if user has access to project - must be author OR project admin
|
||||
const savedViewGroup = await loaders.getSavedViewGroup({
|
||||
projectId,
|
||||
groupId
|
||||
})
|
||||
if (!savedViewGroup) return err(new SavedViewGroupNotFoundError())
|
||||
|
||||
// Prevent default group updates (as it doesnt exist)
|
||||
if (isUngroupedGroup(savedViewGroup.id)) {
|
||||
return err(new UngroupedSavedViewGroupLockError())
|
||||
}
|
||||
|
||||
if (savedViewGroup.authorId !== userId) {
|
||||
const ensuredWriteAccess =
|
||||
await ensureImplicitProjectMemberWithWriteAccessFragment(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
if (ensuredWriteAccess.error.code === ProjectNotEnoughPermissionsError.code)
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Only project owners can update saved view groups they don't own."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ import { FeatureFlags, parseFeatureFlags } from '../environment/index.js'
|
||||
import { mapValues } from 'lodash'
|
||||
import { WorkspacePlan } from '../workspaces/index.js'
|
||||
import { TIME_MS } from '../core/index.js'
|
||||
import { SavedView, SavedViewGroup } from '../authz/domain/savedViews/types.js'
|
||||
import {
|
||||
SavedView,
|
||||
SavedViewGroup,
|
||||
SavedViewVisibility
|
||||
} from '../authz/domain/savedViews/types.js'
|
||||
|
||||
export const fakeGetFactory =
|
||||
<T extends Record<string, unknown>>(defaults: () => T) =>
|
||||
@@ -81,7 +85,8 @@ export const getSavedViewFake = fakeGetFactory<SavedView>(() => ({
|
||||
name: nanoid(10),
|
||||
authorId: nanoid(10),
|
||||
projectId: nanoid(10),
|
||||
groupId: null
|
||||
groupId: null,
|
||||
visibility: SavedViewVisibility.public
|
||||
}))
|
||||
|
||||
export const getSavedViewGroupFake = fakeGetFactory<SavedViewGroup>(() => ({
|
||||
|
||||
Reference in New Issue
Block a user