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:
committed by
GitHub
parent
1994b0b5c4
commit
55f91d2cdf
@@ -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>;
|
||||
|
||||
@@ -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!
|
||||
|
||||
+285
-208
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -112,6 +112,7 @@ export const Scopes = Object.freeze(<const>{
|
||||
},
|
||||
Profile: {
|
||||
Read: 'profile:read',
|
||||
Write: 'profile:write',
|
||||
Email: 'profile:email',
|
||||
Delete: 'profile:delete'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user