fix(gql): scopes, roles, auth (#5724)

* fix(workspace): auto approval
* fix(scopes): access scopes across the server
* fix(hasAccessRole): establish for all mutations
* feat(token): scoping does not require the token to exist
* chore(scopes): added additional roles
* fix: replaced UNAUTHORIZED_ACCESS_ERROR with UNAUTHORIZED
* fix(email): user list scopes
This commit is contained in:
Daniel Gak Anagrov
2025-10-29 10:53:11 +01:00
committed by GitHub
parent 1994b0b5c4
commit 55f91d2cdf
41 changed files with 500 additions and 286 deletions
@@ -501,7 +501,7 @@ export const resolveGenericStatusCode = (
if (errors.some((e) => e.extensions?.code === 'FORBIDDEN')) return 403
if (
errors.some((e) =>
['UNAUTHENTICATED', 'UNAUTHORIZED_ACCESS_ERROR'].includes(
['UNAUTHENTICATED', 'UNAUTHORIZED', 'UNAUTHORIZED_ACCESS_ERROR'].includes(
(e.extensions?.code || '') as string
)
)
@@ -421,7 +421,11 @@ function createWsClient(params: {
const coreShouldSkipLoggingErrors = (err: ErrorResponse): boolean => {
// These fields have special auth requirements and will often throw errors that we don't want to log
const specialAuthFields = ['invitedTeam', 'billing', 'domains', 'subscription']
const specialAuthFieldErrorCodes = ['FORBIDDEN', 'UNAUTHORIZED_ACCESS_ERROR']
const specialAuthFieldErrorCodes = [
'FORBIDDEN',
'UNAUTHORIZED',
'UNAUTHORIZED_ACCESS_ERROR'
]
return !!(
err.graphQLErrors &&
@@ -4,6 +4,7 @@ extend type Query {
"""
streamAccessRequest(streamId: String!): StreamAccessRequest
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use User.projectAccessRequest instead."
)
@@ -8,6 +8,7 @@ extend type Query {
Returns all the publicly available apps on this server.
"""
apps: [ServerAppListItem]
@hasScope(scope: "apps:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future."
)
@@ -330,16 +330,11 @@ extend type ProjectMutations {
type AutomateMutations {
createFunction(input: CreateAutomateFunctionInput!): AutomateFunction!
@hasScope(scope: "automate-functions:write")
createFunctionWithoutVersion(
input: CreateAutomateFunctionWithoutVersionInput!
): AutomateFunctionToken!
@hasScope(scope: "automate-functions:write")
@hasServerRole(role: SERVER_ADMIN)
): AutomateFunctionToken! @hasServerRole(role: SERVER_ADMIN)
updateFunction(input: UpdateAutomateFunctionInput!): AutomateFunction!
@hasScope(scope: "automate-functions:write")
regenerateFunctionToken(functionId: String!): String!
@hasScope(scope: "automate-functions:write")
}
extend type Project {
@@ -369,6 +364,7 @@ extend type Query {
Get a single automate function by id. Error will be thrown if function is not found or inaccessible.
"""
automateFunction(id: ID!): AutomateFunction!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "automate-functions:read")
"""
Part of the automation/function creation handshake mechanism
@@ -385,7 +381,9 @@ extend type Mutation {
): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "automate:report-results")
automateMutations: AutomateMutations! @hasServerRole(role: SERVER_GUEST)
automateMutations: AutomateMutations!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "automate-functions:write")
}
enum ProjectTriggeredAutomationsStatusUpdatedMessageType {
@@ -1,5 +1,6 @@
extend type Query {
comment(id: String!, streamId: String!): Comment
@hasScope(scope: "streams:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use Project.comment instead."
)
@@ -16,6 +17,7 @@ extend type Query {
cursor: String
archived: Boolean! = false
): CommentCollection
@hasScope(scope: "streams:read")
@deprecated(reason: "Use Project/Version/Model 'commentThreads' fields instead")
}
@@ -356,7 +358,9 @@ type CommentMutations {
}
extend type Mutation {
commentMutations: CommentMutations! @hasServerRole(role: SERVER_GUEST)
commentMutations: CommentMutations!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "streams:write")
"""
Used for broadcasting real time chat head bubbles and status. Does not persist any info.
@@ -367,6 +371,7 @@ extend type Mutation {
data: JSONObject
): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "streams:write")
@deprecated(reason: "Use broadcastViewerUserActivity")
"""
@@ -378,6 +383,7 @@ extend type Mutation {
data: JSONObject
): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "streams:write")
@deprecated(reason: "Use broadcastViewerUserActivity")
"""
@@ -46,7 +46,7 @@ extend type Mutation {
projectId: String!
resourceIdString: String!
message: ViewerUserActivityMessageInput!
): Boolean! @hasServerRole(role: SERVER_GUEST)
): Boolean! @hasServerRole(role: SERVER_GUEST) @hasScope(scope: "streams:read")
}
extend type Subscription {
@@ -46,13 +46,15 @@ type AdminQueries {
cursor: String = null
query: String = null
role: ServerRole = null
): AdminUserList! @hasScope(scope: "users:read")
): AdminUserList! @hasServerRole(role: SERVER_ADMIN) @hasScope(scope: "users:read")
inviteList(
limit: Int! = 25
cursor: String = null
query: String = null
): AdminInviteList! @hasScope(scope: "users:invite")
): AdminInviteList!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "users:invite")
projectList(
query: String
@@ -60,9 +62,11 @@ type AdminQueries {
visibility: String
limit: Int! = 25
cursor: String = null
): ProjectCollection!
): ProjectCollection! @hasServerRole(role: SERVER_ADMIN)
serverStatistics: ServerStatistics! @hasScope(scope: "server:stats")
serverStatistics: ServerStatistics!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "server:stats")
}
type AdminMutations {
@@ -72,10 +76,14 @@ type AdminMutations {
with the user before performing this action.
"""
updateEmailVerification(input: AdminUpdateEmailVerificationInput!): Boolean!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "users:email")
}
extend type Query {
admin: AdminQueries! @hasServerRole(role: SERVER_ADMIN)
admin: AdminQueries!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "server:stats")
}
extend type Mutation {
@@ -2,7 +2,9 @@ extend type Query {
"""
If user is authenticated using an app token, this will describe the app
"""
authenticatedAsApp: ServerAppListItem @hasServerRole(role: SERVER_USER)
authenticatedAsApp: ServerAppListItem
@hasServerRole(role: SERVER_USER)
@hasScope(scope: "apps:read")
}
extend type User {
@@ -111,10 +113,14 @@ extend type Mutation {
extend type ProjectMutations {
createEmbedToken(token: EmbedTokenCreateInput!): CreateEmbedTokenReturn!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "tokens:write")
revokeEmbedToken(token: String!, projectId: String!): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "tokens:write")
revokeEmbedTokens(projectId: String!): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "tokens:write")
revokeEmbedTokens(projectId: String!): Boolean! @hasScope(scope: "tokens:write")
}
type EmbedTokenCollection {
@@ -3,7 +3,7 @@ extend type Query {
Find a specific project. Will throw an authorization error if active user isn't authorized
to see it, for example, if a project isn't public and the user doesn't have the appropriate rights.
"""
project(id: String!): Project!
project(id: String!): Project! @hasScope(scope: "streams:read")
}
enum ProjectVisibility {
@@ -188,11 +188,13 @@ type Project {
role: String
createdAt: DateTime!
updatedAt: DateTime!
team: [ProjectCollaborator!]!
team: [ProjectCollaborator!]! @hasScope(scope: "users:read")
"""
Collaborators who have been invited, but not yet accepted.
"""
invitedTeam: [PendingStreamCollaborator!] @hasStreamRole(role: STREAM_CONTRIBUTOR)
invitedTeam: [PendingStreamCollaborator!]
@hasStreamRole(role: STREAM_CONTRIBUTOR)
@hasScope(scope: "users:read")
"""
Source apps used in any models of this project
"""
@@ -4,6 +4,7 @@ extend type Query {
to see it, for example, if a stream isn't public and the user doesn't have the appropriate rights.
"""
stream(id: String!): Stream
@hasScope(scope: "streams:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use Query.project instead."
)
@@ -30,6 +31,7 @@ extend type Query {
limit: Int = 25
): StreamCollection
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "server:stats")
@deprecated(reason: "use admin.projectList instead")
"""
@@ -43,6 +45,7 @@ extend type Query {
"""
sort: DiscoverableStreamsSortingInput
): StreamCollection
@hasScope(scope: "streams:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future."
)
@@ -213,6 +216,7 @@ extend type Mutation {
streamsDelete(ids: [String!]): Boolean!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "streams:write")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use ProjectMutations.batchDelete instead."
)
@@ -240,6 +244,7 @@ extend type Mutation {
# Favorite/unfavorite the given stream
streamFavorite(streamId: String!, favorited: Boolean!): Stream
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:write")
@deprecated(
reason: "Part of the old API surface and will be removed in the future."
)
@@ -249,6 +254,7 @@ extend type Mutation {
"""
streamLeave(streamId: String!): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:write")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use ProjectMutations.leave instead."
)
@@ -2,7 +2,7 @@ extend type Query {
"""
Gets the profile of the authenticated user or null if not authenticated
"""
activeUser: User
activeUser: User @hasScope(scope: "profile:read")
"""
Get the (limited) profile information of another server user
@@ -15,6 +15,7 @@ extend type Query {
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
user(id: String): User
@hasScope(scope: "profile:read")
@deprecated(
reason: "To be removed in the near future! Use 'activeUser' to get info about the active user or 'otherUser' to get info about another user."
)
@@ -42,7 +43,9 @@ extend type Query {
cursor: String
archived: Boolean = false
emailOnly: Boolean = false
): UserSearchResultCollection! @deprecated(reason: "Use users() instead.")
): UserSearchResultCollection!
@hasScope(scope: "users:read")
@deprecated(reason: "Use users() instead.")
"""
Look up server users
@@ -197,6 +200,7 @@ extend type Mutation {
"""
userUpdate(user: UserUpdateInput!): Boolean!
@deprecated(reason: "Use activeUserMutations version")
@hasScope(scope: "profile:write")
"""
Delete a user's account.
@@ -207,14 +211,18 @@ extend type Mutation {
adminDeleteUser(userConfirmation: UserDeleteInput!): Boolean!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "profile:delete")
userRoleChange(userRoleInput: UserRoleInput!): Boolean!
@hasServerRole(role: SERVER_ADMIN)
@hasScope(scope: "profile:write")
"""
Various Active User oriented mutations
"""
activeUserMutations: ActiveUserMutations! @hasServerRole(role: SERVER_GUEST)
activeUserMutations: ActiveUserMutations!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:write")
}
input UserRoleInput {
@@ -1,9 +1,11 @@
extend type Query {
dashboard(id: String!): Dashboard!
dashboard(id: String!): Dashboard! @hasScope(scope: "workspace:read")
}
extend type Mutation {
dashboardMutations: DashboardMutations! @hasServerRole(role: SERVER_GUEST)
dashboardMutations: DashboardMutations!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "workspace:update")
}
type DashboardMutations {
@@ -9,6 +9,8 @@ extend type Mutation {
"""
(Re-)send the account verification e-mail
"""
requestVerification: Boolean! @hasServerRole(role: SERVER_GUEST)
requestVerification: Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:write")
requestVerificationByEmail(email: String!): Boolean!
}
@@ -171,6 +171,7 @@ type FileUploadMutations {
After uploading the file, call mutation startFileImport to register the completed upload.
"""
generateUploadUrl(input: GenerateFileUploadUrlInput!): GenerateFileUploadUrlOutput!
@hasServerRole(role: SERVER_GUEST)
"""
Before calling this mutation, call generateUploadUrl to get the
@@ -179,6 +180,7 @@ type FileUploadMutations {
called to register the completed upload and create the blob metadata.
"""
startFileImport(input: StartFileImportInput!): FileUpload!
@hasServerRole(role: SERVER_GUEST)
"""
Marks the file import flow as completed for that specific job
@@ -186,11 +188,12 @@ type FileUploadMutations {
Mostly for internal service usage.
"""
finishFileImport(input: FinishFileImportInput!): Boolean!
@hasScope(scope: "streams:write")
}
extend type Mutation {
fileUploadMutations: FileUploadMutations!
@hasScope(scope: "streams:write")
@hasServerRole(role: SERVER_GUEST)
}
enum ProjectPendingModelsUpdatedMessageType {
@@ -5,4 +5,5 @@ extend type User {
extend type Mutation {
userNotificationPreferencesUpdate(preferences: JSONObject!): Boolean
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:update")
}
@@ -39,6 +39,7 @@ extend type Mutation {
"""
streamInviteUse(accept: Boolean!, streamId: String!, token: String!): Boolean!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "profile:write")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use ProjectInviteMutations.use instead."
)
@@ -77,6 +78,7 @@ extend type Query {
isn't specified, the server will look for any valid invite.
"""
streamInvite(streamId: String!, token: String): PendingStreamCollaborator
@hasScope(scope: "profile:read")
@deprecated(
reason: "Part of the old API surface and will be removed in the future. Use Query.projectInvite instead."
)
@@ -86,6 +88,7 @@ extend type Query {
isn't specified, the server will look for any valid invite.
"""
projectInvite(projectId: String!, token: String): PendingStreamCollaborator
@hasScope(scope: "profile:read")
"""
Get all invitations to streams that the active user has
@@ -1,5 +1,8 @@
extend type Query {
serverStats: ServerStats! @deprecated(reason: "use admin.serverStatistics instead")
serverStats: ServerStats!
@deprecated(reason: "use admin.serverStatistics instead")
@hasScope(scope: "server:stats")
@hasServerRole(role: SERVER_ADMIN)
}
type ServerStats {
@@ -11,6 +11,8 @@ extend type WorkspaceMutations {
Set the default region where project data will be stored. Only available to admins.
"""
setDefaultRegion(workspaceId: String!, regionKey: String!): Workspace!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "workspace:update")
}
extend type WorkspaceProjectMutations {
@@ -23,4 +25,5 @@ extend type WorkspaceProjectMutations {
moveToRegion(projectId: String!, regionKey: String!): String!
@hasServerRole(role: SERVER_ADMIN)
@hasStreamRole(role: STREAM_OWNER)
@hasScope(scope: "streams:write")
}
@@ -17,11 +17,9 @@ type WorkspaceJoinRequestMutations {
approve(input: ApproveWorkspaceJoinRequestInput!): Boolean!
@hasServerRole(role: SERVER_USER)
@hasScope(scope: "workspace:update")
@hasWorkspaceRole(role: ADMIN)
deny(input: DenyWorkspaceJoinRequestInput!): Boolean!
@hasServerRole(role: SERVER_USER)
@hasScope(scope: "workspace:update")
@hasWorkspaceRole(role: ADMIN)
}
type LimitedWorkspaceJoinRequest {
@@ -11,6 +11,7 @@ extend type Query {
Find workspaces a given user email can use SSO to sign with
"""
workspaceSsoByEmail(email: String!): [LimitedWorkspace!]!
@hasScope(scope: "workspace:read")
"""
Look for an invitation to a workspace, for the current user (authed or not).
@@ -23,7 +24,8 @@ extend type Query {
workspaceId: String
token: String
options: WorkspaceInviteLookupOptions
): PendingWorkspaceCollaborator
): PendingWorkspaceCollaborator @hasScope(scope: "profile:read")
"""
Validates the slug, to make sure it contains only valid characters and its not taken.
"""
@@ -149,21 +151,30 @@ type WorkspaceMutations {
updateRole(input: WorkspaceRoleUpdateInput!): Workspace!
@hasScope(scope: "workspace:update")
@hasServerRole(role: SERVER_USER)
leave(id: ID!): Boolean! @hasServerRole(role: SERVER_GUEST)
leave(id: ID!): Boolean!
@hasScope(scope: "profile:write")
@hasServerRole(role: SERVER_GUEST)
addDomain(input: AddDomainToWorkspaceInput!): Workspace!
@hasScope(scope: "workspace:update")
deleteDomain(input: WorkspaceDomainDeleteInput!): Workspace!
@hasScope(scope: "workspace:update")
deleteSsoProvider(workspaceId: String!): Boolean!
invites: WorkspaceInviteMutations!
projects: WorkspaceProjectMutations! @hasServerRole(role: SERVER_USER)
deleteSsoProvider(workspaceId: String!): Boolean! @hasScope(scope: "workspace:update")
invites: WorkspaceInviteMutations! @hasScope(scope: "users:invite")
projects: WorkspaceProjectMutations!
@hasScope(scope: "streams:write")
@hasServerRole(role: SERVER_USER)
updateCreationState(input: WorkspaceCreationStateInput!): Boolean!
@hasScope(scope: "workspace:update")
updateEmbedOptions(input: WorkspaceUpdateEmbedOptionsInput!): WorkspaceEmbedOptions!
@hasScope(scope: "workspace:update")
"""
Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed"
"""
dismiss(input: WorkspaceDismissInput!): Boolean! @hasServerRole(role: SERVER_USER)
dismiss(input: WorkspaceDismissInput!): Boolean!
@hasServerRole(role: SERVER_USER)
@hasScope(scope: "workspace:update")
requestToJoin(input: WorkspaceRequestToJoinInput!): Boolean!
@hasScope(scope: "profile:write")
@hasServerRole(role: SERVER_USER)
}
@@ -246,13 +257,10 @@ input WorkspaceInviteResendInput {
type WorkspaceInviteMutations {
create(workspaceId: String!, input: WorkspaceInviteCreateInput!): Workspace!
@hasScope(scope: "users:invite")
batchCreate(workspaceId: String!, input: [WorkspaceInviteCreateInput!]!): Workspace!
@hasScope(scope: "users:invite")
use(input: WorkspaceInviteUseInput!): Boolean!
resend(input: WorkspaceInviteResendInput!): Boolean! @hasScope(scope: "users:invite")
resend(input: WorkspaceInviteResendInput!): Boolean!
cancel(workspaceId: String!, inviteId: String!): Workspace!
@hasScope(scope: "users:invite")
@hasServerRole(role: SERVER_USER)
}
@@ -293,14 +301,16 @@ type Workspace {
limit: Int! = 25
cursor: String
filter: WorkspaceTeamFilter
): WorkspaceCollaboratorCollection!
teamByRole: WorkspaceTeamByRole!
): WorkspaceCollaboratorCollection! @hasScope(scope: "users:read")
teamByRole: WorkspaceTeamByRole! @hasScope(scope: "users:read")
"""
Only available to workspace owners/members
"""
invitedTeam(
filter: PendingWorkspaceCollaboratorsFilter
): [PendingWorkspaceCollaborator!] @hasWorkspaceRole(role: MEMBER)
): [PendingWorkspaceCollaborator!]
@hasWorkspaceRole(role: MEMBER)
@hasScope(scope: "users:read")
projects(
limit: Int! = 25
cursor: String
@@ -709,8 +719,11 @@ input AdminAccessToWorkspaceFeatureInput {
extend type AdminMutations {
updateWorkspacePlan(input: AdminUpdateWorkspacePlanInput!): Boolean!
@hasScope(scope: "workspace:update")
giveAccessToWorkspaceFeature(input: AdminAccessToWorkspaceFeatureInput!): Boolean!
@hasScope(scope: "workspace:update")
removeAccessToWorkspaceFeature(input: AdminAccessToWorkspaceFeatureInput!): Boolean!
@hasScope(scope: "workspace:update")
}
extend type ActiveUserMutations {
@@ -133,7 +133,7 @@ describe('GraphQL @apps-api', () => {
const res = await sendRequest(null, { query, variables })
expect(res).to.be.json
expect(res.body.errors).to.exist
expect(res.body.errors[0].extensions?.code).to.equal('FORBIDDEN')
expect(res.body.errors[0].extensions?.code).to.equal('UNAUTHORIZED')
})
it('Should get app info', async () => {
@@ -217,7 +217,7 @@ describe('GraphQL @apps-api', () => {
const query = `mutation del { appDelete( appId: "${testAppId}" ) }`
const res = await sendRequest(null, { query })
expect(res.body.errors).to.exist
expect(res.body.errors[0].extensions?.code).to.equal('FORBIDDEN')
expect(res.body.errors[0].extensions?.code).to.equal('UNAUTHORIZED')
const res2 = await sendRequest(testToken2, { query })
expect(res2.body.errors).to.exist
@@ -139,7 +139,7 @@ const testForbiddenResponse = (
expect(result.errors, 'This should have failed').to.exist
expect(result.errors!.length).to.be.above(0)
expect(result.errors![0].extensions!.code).to.match(
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED_ACCESS_ERROR)/
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED)/
)
}
@@ -13,7 +13,8 @@ export const hasScope: GraphqlDirectiveBuilder = () => {
return {
typeDefs: `
"""
Ensure that the active user's access token has the specified scope allowed for it
Ensure that if there is a token, the token has the specified scope allowed for it
It does not ensure that the token exists, for that, use @hasServerRole
"""
directive @${directiveName}(scope: String!) on FIELD_DEFINITION
`,
@@ -27,8 +28,9 @@ export const hasScope: GraphqlDirectiveBuilder = () => {
const { resolve = defaultFieldResolver } = fieldConfig
fieldConfig.resolve = async function (...args) {
const context = args[2]
const token = context.token
const currentScopes = context.scopes
await validateScopes(currentScopes, requiredScope)
if (token) await validateScopes(currentScopes, requiredScope)
const data = await resolve.apply(this, args)
return data
@@ -48,7 +50,8 @@ export const hasScopes: GraphqlDirectiveBuilder = () => {
return {
typeDefs: `
"""
Ensure that the user's access token has all of the specified scopes allowed for it
Ensure that if there is a token, the token has all of the specified scopes allowed for it
It does not ensure that the token exists, for that, use @hasServerRole
"""
directive @${directiveName}(scopes: [String]!) on FIELD_DEFINITION
`,
@@ -63,13 +66,15 @@ export const hasScopes: GraphqlDirectiveBuilder = () => {
fieldConfig.resolve = async function (...args) {
const context = args[2]
const token = context.token
const currentScopes = context.scopes
await Promise.all(
requiredScopes.map((requiredScope: string) =>
validateScopes(currentScopes, requiredScope)
if (token)
await Promise.all(
requiredScopes.map((requiredScope: string) =>
validateScopes(currentScopes, requiredScope)
)
)
)
const data = await resolve.apply(this, args)
return data
@@ -10333,6 +10333,20 @@ export type RequestToJoinWorkspaceMutationVariables = Exact<{
export type RequestToJoinWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', requestToJoin: boolean } };
export type ApproveJoinRequestMutationVariables = Exact<{
input: ApproveWorkspaceJoinRequestInput;
}>;
export type ApproveJoinRequestMutation = { __typename?: 'Mutation', workspaceJoinRequestMutations: { __typename?: 'WorkspaceJoinRequestMutations', approve: boolean } };
export type DenyJoinRequestMutationVariables = Exact<{
input: DenyWorkspaceJoinRequestInput;
}>;
export type DenyJoinRequestMutation = { __typename?: 'Mutation', workspaceJoinRequestMutations: { __typename?: 'WorkspaceJoinRequestMutations', deny: boolean } };
export type GetWorkspaceWithJoinRequestsQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
filter?: InputMaybe<AdminWorkspaceJoinRequestFilter>;
@@ -11342,6 +11356,8 @@ export const OnWorkspaceProjectsUpdatedDocument = {"kind":"Document","definition
export const OnWorkspaceUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnWorkspaceUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<OnWorkspaceUpdatedSubscription, OnWorkspaceUpdatedSubscriptionVariables>;
export const DismissWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"dismissWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceDismissInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dismiss"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DismissWorkspaceMutation, DismissWorkspaceMutationVariables>;
export const RequestToJoinWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"requestToJoinWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceRequestToJoinInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestToJoin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<RequestToJoinWorkspaceMutation, RequestToJoinWorkspaceMutationVariables>;
export const ApproveJoinRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"approveJoinRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApproveWorkspaceJoinRequestInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceJoinRequestMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"approve"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<ApproveJoinRequestMutation, ApproveJoinRequestMutationVariables>;
export const DenyJoinRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"denyJoinRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DenyWorkspaceJoinRequestInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceJoinRequestMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deny"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DenyJoinRequestMutation, DenyJoinRequestMutationVariables>;
export const GetWorkspaceWithJoinRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithJoinRequests"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"AdminWorkspaceJoinRequestFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"adminWorkspacesJoinRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithJoinRequestsQuery, GetWorkspaceWithJoinRequestsQueryVariables>;
export const GetWorkspaceWithSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentBillingCycleEnd"}},{"kind":"Field","name":{"kind":"Name","value":"billingInterval"}},{"kind":"Field","name":{"kind":"Name","value":"seats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"editors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"available"}},{"kind":"Field","name":{"kind":"Name","value":"assigned"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assigned"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithSubscriptionQuery, GetWorkspaceWithSubscriptionQueryVariables>;
export const GetWorkspacePlanUsageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspacePlanUsage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCount"}},{"kind":"Field","name":{"kind":"Name","value":"modelCount"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<GetWorkspacePlanUsageQuery, GetWorkspacePlanUsageQueryVariables>;
+5
View File
@@ -18,6 +18,11 @@ export default [
description: 'Read your profile information.',
public: true
},
{
name: Scopes.Profile.Write,
description: 'Make actions on your profile.',
public: true
},
{
name: Scopes.Profile.Email,
description: 'Read the email address you registered with.',
@@ -323,9 +323,7 @@ describe('Batch commits', () => {
myCommits.map((c) => c.id)
)
expect(result).to.haveGraphQLErrors(
'Your auth token does not have the required scope'
)
expect(result).to.haveGraphQLErrors('Must provide an auth token')
})
})
})
@@ -195,7 +195,9 @@ describe('Favorite streams', () => {
expect(result.data!.streamFavorite).to.not.be.ok
expect(result.errors).to.have.lengthOf(1)
expect(result.errors!.at(0)!.message).to.contain("doesn't have access")
expect(result.errors!.at(0)!.message).to.contain(
"User doesn't have access to the specified stream"
)
})
describe('and favorited', () => {
@@ -132,6 +132,7 @@ const changeUserRole = changeUserRoleFactory({
'test token user A',
[
Scopes.Server.Setup,
Scopes.Server.Stats,
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Users.Read,
@@ -139,7 +140,9 @@ const changeUserRole = changeUserRoleFactory({
Scopes.Tokens.Write,
Scopes.Tokens.Read,
Scopes.Profile.Read,
Scopes.Profile.Email
Scopes.Profile.Email,
Scopes.Profile.Write,
Scopes.Profile.Delete
]
)}`
@@ -161,7 +164,8 @@ const changeUserRole = changeUserRoleFactory({
Scopes.Tokens.Write,
Scopes.Tokens.Read,
Scopes.Profile.Read,
Scopes.Profile.Email
Scopes.Profile.Write,
Scopes.Profile.Delete
]
)}`
userC = await createTestUser({
@@ -181,7 +185,8 @@ const changeUserRole = changeUserRoleFactory({
Scopes.Tokens.Write,
Scopes.Tokens.Read,
Scopes.Profile.Read,
Scopes.Profile.Email
Scopes.Profile.Email,
Scopes.Profile.Delete
]
)}`
@@ -336,7 +341,7 @@ const changeUserRole = changeUserRoleFactory({
it('Should create some api tokens', async () => {
const res1 = await sendRequest(tokenUserA, {
query:
'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:read", "users:read", "tokens:read"]}) }'
'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:read", "users:read", "tokens:read", "profile:read"]}) }'
})
expect(res1).to.be.json
expect(res1.body.errors).to.not.exist
@@ -509,7 +514,7 @@ const changeUserRole = changeUserRoleFactory({
})
describe('User deletion', () => {
it('Only admins can delete user', async () => {
it('does not allow the endpoint to be used by non-admins', async () => {
const userDelete = await createTestUser({
id: '',
name: 'delete',
@@ -1024,7 +1029,7 @@ const changeUserRole = changeUserRoleFactory({
expect(res3).to.be.json
expect(res3.body.errors).to.exist
expect(res3.body.errors[0].extensions.code).to.equal('FORBIDDEN')
expect(res3.body.errors[0].extensions.code).to.equal('UNAUTHORIZED')
})
it('Should delete a commit', async () => {
@@ -1995,6 +2000,7 @@ const changeUserRole = changeUserRoleFactory({
Scopes.Tokens.Read,
Scopes.Profile.Read,
Scopes.Profile.Email,
Scopes.Profile.Delete,
Scopes.Apps.Read,
Scopes.Apps.Write,
Scopes.Users.Invite
@@ -16,7 +16,7 @@ const testForbiddenResponse = (
expect(result.errors, 'This should have failed').to.exist
expect(result.errors!.length).to.be.above(0)
expect(result.errors![0].extensions!.code, JSON.stringify(result.errors)).to.match(
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED_ACCESS_ERROR)/
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED)/
)
}
@@ -100,9 +100,7 @@ describe('Users (GraphQL)', () => {
const results = await getOtherUser(apollo, { id: otherGuy.id })
expect(results.data?.otherUser).to.be.null
expect(results).to.haveGraphQLErrors(
'Your auth token does not have the required scope'
)
expect(results).to.haveGraphQLErrors('Must provide an auth token')
})
})
@@ -195,7 +195,7 @@ describe('Email verifications @emails', () => {
it('cant request an account verification', async () => {
const result = await requestVerification(apollo)
expect(result).to.haveGraphQLErrors('must provide an auth token')
expect(result).to.haveGraphQLErrors('Must provide an auth token')
expect(result.data?.requestVerification).to.not.be.ok
})
@@ -20,7 +20,7 @@ const testForbiddenResponse = (
expect(result.errors, 'This should have failed').to.exist
expect(result.errors!.length).to.be.above(0)
expect(result.errors![0].extensions!.code, JSON.stringify(result.errors)).to.match(
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED_ACCESS_ERROR)/
/(STREAM_INVALID_ACCESS_ERROR|FORBIDDEN|UNAUTHORIZED)/
)
}
@@ -66,7 +66,7 @@ describe('FileUploads @fileuploads integration', () => {
;({ token: userOneToken } = await createToken({
userId: userOne.id,
name: 'test token',
scopes: [Scopes.Streams.Write]
scopes: [Scopes.Streams.Write, Scopes.Streams.Read]
}))
})
@@ -129,7 +129,7 @@ const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
})
describe('Receive results from file import service', async () => {
it('should 403 if no auth token is provided', async () => {
it('should 401 if no auth token is provided', async () => {
const sucessPayload = {
projectId: projectOneId,
jobId: jobOneId,
@@ -147,8 +147,8 @@ const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
expect(haveErrors(gqlResponse))
expect(gqlResponse.body).to.nested.include({
'errors[0].extensions.code': 'FORBIDDEN',
'errors[0].extensions.statusCode': 403
'errors[0].extensions.code': 'UNAUTHORIZED',
'errors[0].extensions.statusCode': 401
})
})
@@ -31,7 +31,7 @@ export class LogicError extends BaseError {
* Aka NonAuthorizedError or NotAuthorizedError
*/
export class UnauthorizedError extends BaseError {
static code = 'UNAUTHORIZED_ACCESS_ERROR'
static code = 'UNAUTHORIZED'
static defaultMessage = 'Attempted unauthorized access to data'
static statusCode = 401
}
@@ -45,7 +45,12 @@ import {
buildBasicTestModel,
buildBasicTestProject
} from '@/modules/core/tests/helpers/creation'
import { BadRequestError, ForbiddenError, NotFoundError } from '@/modules/shared/errors'
import {
BadRequestError,
ForbiddenError,
NotFoundError,
UnauthorizedError
} from '@/modules/shared/errors'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import type { FactoryResultOf } from '@/modules/shared/helpers/factory'
import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
@@ -356,12 +361,12 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
if (FF_WORKSPACES_MODULE_ENABLED) {
describe('creation', () => {
describe('auth policy checks', () => {
it('should fail with ForbiddenError if user is not logged in', async () => {
it('should fail with Unauthorized if user is not logged in', async () => {
const res = await createSavedView(
buildCreateInput({ projectId: myProject.id, resourceIdString: 'abc' }),
{ authUserId: null }
)
expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code })
expect(res).to.haveGraphQLErrors({ code: UnauthorizedError.code })
expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok
})
@@ -12,6 +12,7 @@ import {
createWorkspaceSeatFactory,
getWorkspaceUserSeatFactory
} from '@/modules/gatekeeper/repositories/workspaceSeat'
import { authorizeResolver } from '@/modules/shared'
import { commandFactory } from '@/modules/shared/command'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
@@ -50,6 +51,7 @@ import {
import type { WorkspaceJoinRequestStatus } from '@/modules/workspacesCore/domain/types'
import type { WorkspaceJoinRequestGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { Roles } from '@speckle/shared'
const eventBus = getEventBus()
@@ -155,6 +157,13 @@ export default FF_WORKSPACES_MODULE_ENABLED
targetUserId
})
await authorizeResolver(
ctx.userId,
workspaceId,
Roles.Workspace.Admin,
ctx.resourceAccessRules
)
const approveWorkspaceJoinRequest =
commandFactory<ApproveWorkspaceJoinRequest>({
db,
@@ -229,6 +238,14 @@ export default FF_WORKSPACES_MODULE_ENABLED
workspaceId,
targetUserId
})
await authorizeResolver(
ctx.userId,
workspaceId,
Roles.Workspace.Admin,
ctx.resourceAccessRules
)
const denyWorkspaceJoinRequest = commandFactory<DenyWorkspaceJoinRequest>({
db,
operationFactory: ({ db }) => {
@@ -335,6 +335,22 @@ export const requestToJoinWorkspaceMutation = gql`
}
`
export const approveJoinRequestMutation = gql`
mutation approveJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
approve(input: $input)
}
}
`
export const denyJoinRequestMutation = gql`
mutation denyJoinRequest($input: DenyWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
deny(input: $input)
}
}
`
export const getWorkspaceWithJoinRequestsQuery = gql`
query GetWorkspaceWithJoinRequests(
$workspaceId: String!
@@ -1,278 +1,355 @@
import { db } from '@/db/knex'
import { createRandomString } from '@/modules/core/helpers/testHelpers'
import type { BasicTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, login } from '@/test/authHelper'
import {
ApproveJoinRequestDocument,
DenyJoinRequestDocument,
DismissWorkspaceDocument,
GetActiveUserWithWorkspaceJoinRequestsDocument,
GetWorkspaceTeamDocument,
GetWorkspaceWithJoinRequestsDocument,
RequestToJoinWorkspaceDocument
} from '@/modules/core/graph/generated/graphql'
import { beforeEachContext } from '@/test/hooks'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import { upsertWorkspaceRoleFactory } from '@/modules/workspaces/repositories/workspaces'
before(async () => {
await beforeEachContext()
})
describe('WorkspaceJoinRequests GQL', () => {
describe('Workspace.adminWorkspacesJoinRequests', () => {
it('should return the workspace join requests for the admin', async () => {
const admin = await createTestUser({
let admin: BasicTestUser
let user1: BasicTestUser
let user2: BasicTestUser
let user3: BasicTestUser
let workspace1: BasicTestWorkspace
let workspace2: BasicTestWorkspace
let dismissedWorkspace: BasicTestWorkspace
let workspaceAutoJoin: BasicTestWorkspace
before(async () => {
await beforeEachContext()
;[admin, user1, user2, user3] = await Promise.all([
createTestUser({
name: 'admin user',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const user1 = await createTestUser({
}),
createTestUser({
name: 'user 1',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const user2 = await createTestUser({
}),
createTestUser({
name: 'user 2',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
}),
createTestUser({
name: 'user 3',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
])
;[workspace1, dismissedWorkspace, workspace2, workspaceAutoJoin] =
await Promise.all([
await createTestWorkspace(
{
id: createRandomString(),
name: 'Workspace 1',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
},
admin,
{ domain: 'example.org' }
),
await createTestWorkspace(
{
id: createRandomString(),
name: 'should not be visible',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
},
admin,
{
domain: 'example.org'
}
),
await createTestWorkspace(
{
id: createRandomString(),
name: 'Workspace 2',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
},
admin,
{ domain: 'example.org' }
),
await createTestWorkspace(
{
id: createRandomString(),
name: 'Worksapce autojoin',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true,
discoverabilityAutoJoinEnabled: true
},
admin,
{
domain: 'example.org'
}
)
])
})
const workspace1 = {
id: createRandomString(),
name: 'Workspace 1',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace1, admin, { domain: 'example.org' })
const dismissedWorkspace = {
id: createRandomString(),
name: 'should not be visible',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(dismissedWorkspace, admin, { domain: 'example.org' })
const workspace2 = {
id: createRandomString(),
name: 'Workspace 2',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace2, admin, { domain: 'example.org' })
const nobodyWorkspace = {
id: createRandomString(),
name: 'nobody',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(nobodyWorkspace, admin, { domain: 'example.org' })
const nonAdminWorkspace = {
id: createRandomString(),
name: 'nonadmin',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(nonAdminWorkspace, admin, { domain: 'example.org' })
await upsertWorkspaceRoleFactory({ db })({
userId: admin.id,
workspaceId: nonAdminWorkspace.id,
role: Roles.Workspace.Member,
createdAt: new Date()
})
describe('Workspace.adminWorkspacesJoinRequests', () => {
it('allows users to request joining a workspace', async () => {
// User1 requests to join workspace1
const sessionUser1 = await login(user1)
const joinReq1 = await sessionUser1.execute(RequestToJoinWorkspaceDocument, {
input: {
workspaceId: workspace1.id
}
})
expect(joinReq1).to.not.haveGraphQLErrors()
// User2 requests to join workspace2
const sessionUser2 = await login(user2)
const joinReq2 = await sessionUser2.execute(RequestToJoinWorkspaceDocument, {
input: {
workspaceId: workspace2.id
}
})
expect(joinReq2).to.not.haveGraphQLErrors()
// User requests to join dismissedWorkspace
const joinReqDismissed = await sessionUser2.execute(
await sessionUser1.execute(
RequestToJoinWorkspaceDocument,
{
input: {
workspaceId: dismissedWorkspace.id
}
}
{ input: { workspaceId: workspace1.id } },
{ assertNoErrors: true }
)
expect(joinReqDismissed).to.not.haveGraphQLErrors()
const dismissReq = await sessionUser2.execute(DismissWorkspaceDocument, {
input: {
workspaceId: dismissedWorkspace.id
}
})
expect(dismissReq).to.not.haveGraphQLErrors()
// admin logs in
const sessionAdmin = await login(admin)
const workspace1Res = await sessionAdmin.execute(
GetWorkspaceWithJoinRequestsDocument,
{
workspaceId: workspace1.id
}
{ workspaceId: workspace1.id },
{ assertNoErrors: true }
)
expect(workspace1Res).to.not.haveGraphQLErrors()
const { items: items1, totalCount: totalCount1 } =
// has one join request
const { items: items, totalCount: totalCount } =
workspace1Res.data!.workspace!.adminWorkspacesJoinRequests!
expect(totalCount1).to.equal(1)
expect(totalCount).to.equal(1)
expect(items).to.have.length(1)
expect(items[0].status).to.equal('pending')
expect(items[0].workspace.id).to.equal(workspace1.id)
expect(items[0].user.id).to.equal(user1.id)
})
expect(items1).to.have.length(1)
expect(items1[0].status).to.equal('pending')
expect(items1[0].workspace.id).to.equal(workspace1.id)
expect(items1[0].user.id).to.equal(user1.id)
const workspace2Res = await sessionAdmin.execute(
GetWorkspaceWithJoinRequestsDocument,
{
workspaceId: workspace2.id
}
it('has the ability to dismiss a join request', async () => {
// User2 requests to join dismissedWorkspace
const sessionUser2 = await login(user2)
await sessionUser2.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: dismissedWorkspace.id } },
{ assertNoErrors: true }
)
expect(workspace2Res).to.not.haveGraphQLErrors()
const { items: items2, totalCount: totalCount2 } =
workspace2Res.data!.workspace!.adminWorkspacesJoinRequests!
// admins sees a request
const sessionAdmin = await login(admin)
const joinRequests = await sessionAdmin.execute(
GetWorkspaceWithJoinRequestsDocument,
{ workspaceId: dismissedWorkspace.id },
{ assertNoErrors: true }
)
const { workspace: joinsWorkspace2 } = joinRequests.data!
expect(joinsWorkspace2!.adminWorkspacesJoinRequests!.totalCount).to.equal(1)
expect(totalCount2).to.equal(1)
expect(items2).to.have.length(1)
expect(items2[0].status).to.equal('pending')
expect(items2[0].workspace.id).to.equal(workspace2.id)
expect(items2[0].user.id).to.equal(user2.id)
// user2 cancels the request
await sessionUser2.execute(
DismissWorkspaceDocument,
{ input: { workspaceId: dismissedWorkspace.id } },
{ assertNoErrors: true }
)
// no request for admin
const workspaceDismissedRes = await sessionAdmin.execute(
GetWorkspaceWithJoinRequestsDocument,
{
workspaceId: dismissedWorkspace.id
}
{ workspaceId: dismissedWorkspace.id },
{ assertNoErrors: true }
)
expect(workspaceDismissedRes).to.not.haveGraphQLErrors()
const { items: itemsDismissed, totalCount: totalCountDismissed } =
workspaceDismissedRes.data!.workspace!.adminWorkspacesJoinRequests!
expect(totalCountDismissed).to.equal(0)
expect(itemsDismissed).to.have.length(0)
const { workspace } = workspaceDismissedRes.data!
expect(workspace.adminWorkspacesJoinRequests!.items).to.have.lengthOf(0)
expect(workspace.adminWorkspacesJoinRequests!.totalCount).to.eql(0)
})
})
describe('User.workspaceJoinRequests', () => {
it('should return the workspace join requests for the user', async () => {
const admin = await createTestUser({
name: 'admin user',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const user = await createTestUser({
name: 'user 1',
role: Roles.Server.User,
email: `${createRandomString()}@example.org`,
verified: true
})
const workspace1 = {
id: createRandomString(),
name: 'Workspace 1',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace1, admin, { domain: 'example.org' })
const workspace2 = {
id: createRandomString(),
name: 'Workspace 2',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspace2, admin, { domain: 'example.org' })
const workspaceDismissed = {
id: createRandomString(),
name: 'should not see',
ownerId: admin.id,
description: '',
discoverabilityEnabled: true
}
await createTestWorkspace(workspaceDismissed, admin, { domain: 'example.org' })
const sessionUser = await login(user)
// User requests to join workspace1
const joinReq1 = await sessionUser.execute(RequestToJoinWorkspaceDocument, {
input: {
workspaceId: workspace1.id
}
})
expect(joinReq1).to.not.haveGraphQLErrors()
// User requests to join workspace2
const joinReq2 = await sessionUser.execute(RequestToJoinWorkspaceDocument, {
input: {
workspaceId: workspace2.id
}
})
expect(joinReq2).to.not.haveGraphQLErrors()
// User requests to join workspaceDismissed
const joinReqDismissed = await sessionUser.execute(
// User requests to join workspace1 and 2
const sessionUser = await login(user1)
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{
input: {
workspaceId: workspaceDismissed.id
}
}
{ input: { workspaceId: workspace1.id } },
{ assertNoErrors: true }
)
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: workspace2.id } },
{ assertNoErrors: true }
)
expect(joinReqDismissed).to.not.haveGraphQLErrors()
const dismissReq = await sessionUser.execute(DismissWorkspaceDocument, {
input: {
workspaceId: workspaceDismissed.id
}
})
expect(dismissReq).to.not.haveGraphQLErrors()
const res = await sessionUser.execute(
GetActiveUserWithWorkspaceJoinRequestsDocument,
{}
{},
{ assertNoErrors: true }
)
expect(res).to.not.haveGraphQLErrors()
const { items, totalCount } = res.data!.activeUser!.workspaceJoinRequests!
expect(totalCount).to.equal(2)
expect(items).to.have.length(2)
expect(items[0].status).to.equal('pending')
expect(items[0].workspace.id).to.equal(workspace2.id)
expect(items[0].user.id).to.equal(user.id)
expect(items[0].user.id).to.equal(user1.id)
expect(items[1].status).to.equal('pending')
expect(items[1].workspace.id).to.equal(workspace1.id)
expect(items[1].user.id).to.equal(user.id)
expect(items[1].user.id).to.equal(user1.id)
})
it('does not show request that were dissmissed for the user', async () => {
// User requests to join workspaceDismissed
const sessionUser = await login(user2)
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: dismissedWorkspace.id } },
{ assertNoErrors: true }
)
// dismisses it
await sessionUser.execute(
DismissWorkspaceDocument,
{ input: { workspaceId: dismissedWorkspace.id } },
{ assertNoErrors: true }
)
const res = await sessionUser.execute(
GetActiveUserWithWorkspaceJoinRequestsDocument,
{},
{ assertNoErrors: true }
)
const { items, totalCount } = res.data!.activeUser!.workspaceJoinRequests!
expect(totalCount).to.equal(0)
expect(items).to.have.length(0)
})
})
describe('joining a workspace', () => {
it('allows admin accepting a join request to a workspace', async () => {
const sessionAdmin = await login(admin)
const sessionUser = await login(user1)
// User requests to join workspace1
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: workspace1.id } },
{ assertNoErrors: true }
)
await sessionAdmin.execute(
ApproveJoinRequestDocument,
{ input: { workspaceId: workspace1.id, userId: user1.id } },
{ assertNoErrors: true }
)
const res = await sessionAdmin.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace1.id
})
const { items, totalCount } = res.data!.workspace.team
expect(totalCount).to.equal(2)
expect(items).to.have.length(2)
expect(items[0].id).to.equal(user1.id)
})
it('allows admin denying a join request to a workspace', async () => {
const sessionAdmin = await login(admin)
const sessionUser = await login(user2)
// User requests to join workspace1
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: workspace2.id } },
{ assertNoErrors: true }
)
await sessionAdmin.execute(
DenyJoinRequestDocument,
{ input: { workspaceId: workspace2.id, userId: user2.id } },
{ assertNoErrors: true }
)
const res = await sessionAdmin.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace2.id
})
const { items, totalCount } = res.data!.workspace.team
expect(totalCount).to.equal(1)
expect(items).to.have.length(1)
})
it('doesnt allow the joiner user to hack around their way into a workspace', async () => {
const sessionAdmin = await login(admin)
const sessionUser = await login(user3)
// User requests to join workspace1
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: workspace2.id } },
{ assertNoErrors: true }
)
// Accepts himself
const autoAcceptAttempt = await sessionUser.execute(
ApproveJoinRequestDocument,
{ input: { workspaceId: workspace2.id, userId: user3.id } },
{ assertNoErrors: false }
)
const autoDenyAttempt = await sessionUser.execute(
DenyJoinRequestDocument,
{ input: { workspaceId: workspace2.id, userId: user3.id } },
{ assertNoErrors: false }
)
const res = await sessionAdmin.execute(GetWorkspaceTeamDocument, {
workspaceId: workspace2.id
})
const { items, totalCount } = res.data!.workspace.team
const AUTH_ERROR = 'You are not authorized to access this resource.'
expect(autoAcceptAttempt).to.haveGraphQLErrors()
expect(autoAcceptAttempt.errors![0].message).to.contain(AUTH_ERROR)
expect(autoDenyAttempt).to.haveGraphQLErrors()
expect(autoDenyAttempt.errors![0].message).to.contain(AUTH_ERROR)
expect(totalCount).to.equal(1)
expect(items).to.have.length(1)
})
it('can auto join if admin had previously preconfigured it', async () => {
const sessionAdmin = await login(admin)
const sessionUser = await login(user3)
// User requests to join workspace1
await sessionUser.execute(
RequestToJoinWorkspaceDocument,
{ input: { workspaceId: workspaceAutoJoin.id } },
{ assertNoErrors: true }
)
const res = await sessionAdmin.execute(GetWorkspaceTeamDocument, {
workspaceId: workspaceAutoJoin.id
})
const { items, totalCount } = res.data!.workspace.team
expect(totalCount).to.equal(2)
expect(items).to.have.length(2)
expect(items[0].id).to.equal(user3.id)
})
})
})
+1
View File
@@ -112,6 +112,7 @@ export const Scopes = Object.freeze(<const>{
},
Profile: {
Read: 'profile:read',
Write: 'profile:write',
Email: 'profile:email',
Delete: 'profile:delete'
},