Merge pull request #1728 from specklesystems/gergo/serverGuest/main
Server Guest Role
This commit is contained in:
@@ -333,6 +333,7 @@ jobs:
|
||||
command: server /data --console-address ":9001"
|
||||
# environment:
|
||||
|
||||
resource_class: large
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test'
|
||||
|
||||
@@ -17,6 +17,7 @@ const { spawn } = require('child_process')
|
||||
const ServerAPI = require('../ifc/api')
|
||||
const objDependencies = require('./objDependencies')
|
||||
const { logger } = require('../observability/logging')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
@@ -97,7 +98,7 @@ async function doTask(task) {
|
||||
const { token } = await serverApi.createToken({
|
||||
userId: info.userId,
|
||||
name: 'temp upload token',
|
||||
scopes: ['streams:write', 'streams:read'],
|
||||
scopes: [Scopes.Streams.Write, Scopes.Streams.Read],
|
||||
lifespan: 1000000
|
||||
})
|
||||
tempUserToken = token
|
||||
|
||||
@@ -143,7 +143,8 @@ export default {
|
||||
roleLookupTable: {
|
||||
[Roles.Server.User]: 'User',
|
||||
[Roles.Server.Admin]: 'Admin',
|
||||
[Roles.Server.ArchivedUser]: 'Archived'
|
||||
[Roles.Server.ArchivedUser]: 'Archived',
|
||||
[Roles.Server.Guest]: 'Guest'
|
||||
},
|
||||
adminUsers: {
|
||||
items: [],
|
||||
|
||||
@@ -3,7 +3,7 @@ extend type Query {
|
||||
Get authed user's stream access request
|
||||
"""
|
||||
streamAccessRequest(streamId: String!): StreamAccessRequest
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
extend type Stream {
|
||||
@@ -21,13 +21,13 @@ extend type Mutation {
|
||||
requestId: String!
|
||||
accept: Boolean!
|
||||
role: StreamRole! = STREAM_CONTRIBUTOR
|
||||
): Boolean! @hasRole(role: "server:user") @hasScope(scope: "users:invite")
|
||||
): Boolean! @hasServerRole(role: SERVER_GUEST) @hasScope(scope: "users:invite")
|
||||
|
||||
"""
|
||||
Request access to a specific stream
|
||||
"""
|
||||
streamAccessRequestCreate(streamId: String!): StreamAccessRequest!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "users:invite")
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ extend type User {
|
||||
before: DateTime
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection @hasRole(role: "server:user") @hasScope(scope: "users:read")
|
||||
): ActivityCollection
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "users:read")
|
||||
|
||||
"""
|
||||
The user's timeline in chronological order
|
||||
@@ -19,7 +21,7 @@ extend type User {
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScopes(scopes: ["users:read", "streams:read"])
|
||||
}
|
||||
|
||||
@@ -33,7 +35,9 @@ extend type LimitedUser {
|
||||
before: DateTime
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection @hasRole(role: "server:user") @hasScope(scope: "users:read")
|
||||
): ActivityCollection
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "users:read")
|
||||
|
||||
"""
|
||||
The user's timeline in chronological order
|
||||
@@ -44,7 +48,7 @@ extend type LimitedUser {
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScopes(scopes: ["users:read", "streams:read"])
|
||||
}
|
||||
|
||||
@@ -58,7 +62,9 @@ extend type Stream {
|
||||
before: DateTime
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection @hasRole(role: "server:user") @hasScope(scope: "streams:read")
|
||||
): ActivityCollection
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
extend type Branch {
|
||||
@@ -71,7 +77,9 @@ extend type Branch {
|
||||
before: DateTime
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection @hasRole(role: "server:user") @hasScope(scope: "streams:read")
|
||||
): ActivityCollection
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
extend type Commit {
|
||||
@@ -84,7 +92,9 @@ extend type Commit {
|
||||
before: DateTime
|
||||
cursor: DateTime
|
||||
limit: Int! = 25
|
||||
): ActivityCollection @hasRole(role: "server:user") @hasScope(scope: "streams:read")
|
||||
): ActivityCollection
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
type ActivityCollection {
|
||||
|
||||
@@ -47,13 +47,15 @@ extend type User {
|
||||
Returns the apps you have authorized.
|
||||
"""
|
||||
authorizedApps: [ServerAppListItem]
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "apps:read")
|
||||
|
||||
"""
|
||||
Returns the apps you have created.
|
||||
"""
|
||||
createdApps: [ServerApp!] @hasRole(role: "server:user") @hasScope(scope: "apps:read")
|
||||
createdApps: [ServerApp!]
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "apps:read")
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
@@ -61,28 +63,28 @@ extend type Mutation {
|
||||
Register a new third party application.
|
||||
"""
|
||||
appCreate(app: AppCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "apps:write")
|
||||
|
||||
"""
|
||||
Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.**
|
||||
"""
|
||||
appUpdate(app: AppUpdateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "apps:write")
|
||||
|
||||
"""
|
||||
Deletes a thirty party application.
|
||||
"""
|
||||
appDelete(appId: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "apps:write")
|
||||
|
||||
"""
|
||||
Revokes (de-authorizes) an application that you have previously authorized.
|
||||
"""
|
||||
appRevokeAccess(appId: String!): Boolean
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "apps:write")
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ type CommentMutations {
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
commentMutations: CommentMutations! @hasServerRole(role: SERVER_USER)
|
||||
commentMutations: CommentMutations! @hasServerRole(role: SERVER_GUEST)
|
||||
|
||||
"""
|
||||
Used for broadcasting real time chat head bubbles and status. Does not persist any info.
|
||||
@@ -334,7 +334,7 @@ extend type Mutation {
|
||||
resourceId: String!
|
||||
data: JSONObject
|
||||
): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@deprecated(reason: "Use broadcastViewerUserActivity")
|
||||
|
||||
"""
|
||||
@@ -345,14 +345,14 @@ extend type Mutation {
|
||||
commentId: String!
|
||||
data: JSONObject
|
||||
): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@deprecated(reason: "Use broadcastViewerUserActivity")
|
||||
|
||||
"""
|
||||
Creates a comment
|
||||
"""
|
||||
commentCreate(input: CommentCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use commentMutations version")
|
||||
|
||||
@@ -360,7 +360,7 @@ extend type Mutation {
|
||||
Flags a comment as viewed by you (the logged in user).
|
||||
"""
|
||||
commentView(streamId: String!, commentId: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use commentMutations version")
|
||||
|
||||
@@ -372,7 +372,7 @@ extend type Mutation {
|
||||
commentId: String!
|
||||
archived: Boolean! = true
|
||||
): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use commentMutations version")
|
||||
|
||||
@@ -380,7 +380,7 @@ extend type Mutation {
|
||||
Edits a comment.
|
||||
"""
|
||||
commentEdit(input: CommentEditInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use commentMutations version")
|
||||
|
||||
@@ -388,7 +388,7 @@ extend type Mutation {
|
||||
Adds a reply to a comment.
|
||||
"""
|
||||
commentReply(input: ReplyCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use commentMutations version")
|
||||
}
|
||||
@@ -457,7 +457,7 @@ extend type Subscription {
|
||||
- for a specific resource/set of resources: pass in a list of resourceIds (commit or object ids); this sub will get called when *any* of the resources provided get a comment.
|
||||
"""
|
||||
commentActivity(streamId: String!, resourceIds: [String]): CommentActivityMessage!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(reason: "Use projectCommentsUpdated")
|
||||
|
||||
@@ -470,7 +470,7 @@ extend type Subscription {
|
||||
streamId: String!
|
||||
commentId: String!
|
||||
): CommentThreadActivityMessage!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
@deprecated(
|
||||
reason: "Use projectCommentsUpdated or viewerUserActivityBroadcasted for reply status"
|
||||
|
||||
@@ -46,7 +46,7 @@ extend type Mutation {
|
||||
projectId: String!
|
||||
resourceIdString: String!
|
||||
message: ViewerUserActivityMessageInput!
|
||||
): Boolean! @hasServerRole(role: SERVER_USER)
|
||||
): Boolean! @hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
extend type Subscription {
|
||||
|
||||
@@ -2,7 +2,9 @@ extend type User {
|
||||
"""
|
||||
Returns a list of your personal api tokens.
|
||||
"""
|
||||
apiTokens: [ApiToken] @hasRole(role: "server:user") @hasScope(scope: "tokens:read")
|
||||
apiTokens: [ApiToken]
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "tokens:read")
|
||||
}
|
||||
|
||||
type ApiToken {
|
||||
@@ -26,12 +28,12 @@ extend type Mutation {
|
||||
Creates an personal api token.
|
||||
"""
|
||||
apiTokenCreate(token: ApiTokenCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "tokens:write")
|
||||
"""
|
||||
Revokes (deletes) an personal api token.
|
||||
"""
|
||||
apiTokenRevoke(token: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "tokens:write")
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ type Commit {
|
||||
Will throw an authorization error if active user isn't authorized to see it, for example,
|
||||
if a stream isn't public and the user doesn't have the appropriate rights.
|
||||
"""
|
||||
stream: Stream! @hasRole(role: "server:user") @hasScope(scope: "streams:read")
|
||||
stream: Stream! @hasServerRole(role: SERVER_GUEST) @hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
type BranchCollection {
|
||||
@@ -65,40 +65,40 @@ type CommitCollection {
|
||||
|
||||
extend type Mutation {
|
||||
branchCreate(branch: BranchCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
branchUpdate(branch: BranchUpdateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
branchDelete(branch: BranchDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
commitCreate(commit: CommitCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
commitUpdate(commit: CommitUpdateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
commitReceive(input: CommitReceivedInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
commitDelete(commit: CommitDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
"""
|
||||
Move a batch of commits to a new branch
|
||||
"""
|
||||
commitsMove(input: CommitsMoveInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
"""
|
||||
Delete a batch of commits
|
||||
"""
|
||||
commitsDelete(input: CommitsDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
}
|
||||
|
||||
@@ -108,38 +108,38 @@ extend type Subscription {
|
||||
Subscribe to branch created event
|
||||
"""
|
||||
branchCreated(streamId: String!): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
"""
|
||||
Subscribe to branch updated event.
|
||||
"""
|
||||
branchUpdated(streamId: String!, branchId: String): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
"""
|
||||
Subscribe to branch deleted event
|
||||
"""
|
||||
branchDeleted(streamId: String!): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
Subscribe to commit created event
|
||||
"""
|
||||
commitCreated(streamId: String!): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
"""
|
||||
Subscribe to commit updated event.
|
||||
"""
|
||||
commitUpdated(streamId: String!, commitId: String): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
"""
|
||||
Subscribe to commit deleted event
|
||||
"""
|
||||
commitDeleted(streamId: String!): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
|
||||
@@ -164,11 +164,11 @@ type VersionMutations {
|
||||
|
||||
extend type Mutation {
|
||||
modelMutations: ModelMutations!
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
versionMutations: VersionMutations!
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:write")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ type ServerInfo {
|
||||
roles: [Role]!
|
||||
scopes: [Scope]!
|
||||
inviteOnly: Boolean
|
||||
guestModeEnabled: Boolean!
|
||||
version: String
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ type Scope {
|
||||
|
||||
extend type Mutation {
|
||||
serverInfoUpdate(info: ServerInfoUpdateInput!): Boolean
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "server:setup")
|
||||
}
|
||||
|
||||
@@ -48,4 +49,5 @@ input ServerInfoUpdateInput {
|
||||
adminContact: String
|
||||
termsOfService: String
|
||||
inviteOnly: Boolean
|
||||
guestModeEnabled: Boolean
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ extend type Query {
|
||||
Pass in the `query` parameter to search by name, description or ID.
|
||||
"""
|
||||
streams(query: String, limit: Int = 25, cursor: String): StreamCollection
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
@@ -23,7 +23,7 @@ extend type Query {
|
||||
visibility: String
|
||||
limit: Int = 25
|
||||
): StreamCollection
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@deprecated(reason: "use admin.projectList instead")
|
||||
|
||||
"""
|
||||
@@ -79,7 +79,7 @@ extend type User {
|
||||
authenticated user, then this will only return discoverable streams.
|
||||
"""
|
||||
streams(limit: Int! = 25, cursor: String): StreamCollection!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
@@ -87,7 +87,7 @@ extend type User {
|
||||
Note: You can't use this to retrieve another user's favorite streams.
|
||||
"""
|
||||
favoriteStreams(limit: Int! = 25, cursor: String): StreamCollection!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
@@ -101,7 +101,7 @@ extend type LimitedUser {
|
||||
Returns all discoverable streams that the user is a collaborator on
|
||||
"""
|
||||
streams(limit: Int! = 25, cursor: String): StreamCollection!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
@@ -152,43 +152,43 @@ extend type Mutation {
|
||||
Creates a new stream.
|
||||
"""
|
||||
streamCreate(stream: StreamCreateInput!): String
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
"""
|
||||
Updates an existing stream.
|
||||
"""
|
||||
streamUpdate(stream: StreamUpdateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
"""
|
||||
Deletes an existing stream.
|
||||
"""
|
||||
streamDelete(id: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
streamsDelete(ids: [String!]): Boolean! @hasRole(role: "server:admin")
|
||||
streamsDelete(ids: [String!]): Boolean! @hasServerRole(role: SERVER_ADMIN)
|
||||
"""
|
||||
Update permissions of a user on a given stream.
|
||||
"""
|
||||
streamUpdatePermission(permissionParams: StreamUpdatePermissionInput!): Boolean
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
"""
|
||||
Revokes the permissions of a user on a given stream.
|
||||
"""
|
||||
streamRevokePermission(permissionParams: StreamRevokePermissionInput!): Boolean
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
# Favorite/unfavorite the given stream
|
||||
streamFavorite(streamId: String!, favorited: Boolean!): Stream
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
|
||||
"""
|
||||
Remove yourself from stream collaborators (not possible for the owner)
|
||||
"""
|
||||
streamLeave(streamId: String!): Boolean! @hasRole(role: "server:user")
|
||||
streamLeave(streamId: String!): Boolean! @hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
extend type Subscription {
|
||||
@@ -202,7 +202,7 @@ extend type Subscription {
|
||||
**NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload.
|
||||
"""
|
||||
userStreamAdded: JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "profile:read")
|
||||
|
||||
"""
|
||||
@@ -210,7 +210,7 @@ extend type Subscription {
|
||||
**NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload.
|
||||
"""
|
||||
userStreamRemoved: JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "profile:read")
|
||||
|
||||
#
|
||||
@@ -222,14 +222,14 @@ extend type Subscription {
|
||||
Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
|
||||
"""
|
||||
streamUpdated(streamId: String): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
|
||||
"""
|
||||
Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream.
|
||||
"""
|
||||
streamDeleted(streamId: String): JSONObject
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ extend type Query {
|
||||
Get the (limited) profile information of another server user
|
||||
"""
|
||||
otherUser(id: String!): LimitedUser
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "users:read")
|
||||
|
||||
"""
|
||||
@@ -28,8 +28,8 @@ extend type Query {
|
||||
offset: Int! = 0
|
||||
query: String = null
|
||||
): AdminUsersListCollection
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@deprecated(reason: "use admin.UserList instead")
|
||||
@hasRole(role: "server:admin")
|
||||
@hasScope(scope: "users:read")
|
||||
|
||||
"""
|
||||
@@ -154,18 +154,19 @@ extend type Mutation {
|
||||
Delete a user's account.
|
||||
"""
|
||||
userDelete(userConfirmation: UserDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "profile:delete")
|
||||
|
||||
adminDeleteUser(userConfirmation: UserDeleteInput!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
|
||||
userRoleChange(userRoleInput: UserRoleInput!): Boolean! @hasRole(role: "server:admin")
|
||||
userRoleChange(userRoleInput: UserRoleInput!): Boolean!
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
|
||||
"""
|
||||
Various Active User oriented mutations
|
||||
"""
|
||||
activeUserMutations: ActiveUserMutations! @hasRole(role: "server:user")
|
||||
activeUserMutations: ActiveUserMutations! @hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
input UserRoleInput {
|
||||
|
||||
@@ -9,5 +9,5 @@ extend type Mutation {
|
||||
"""
|
||||
(Re-)send the account verification e-mail
|
||||
"""
|
||||
requestVerification: Boolean! @hasRole(role: "server:user")
|
||||
requestVerification: Boolean! @hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ extend type User {
|
||||
|
||||
extend type Mutation {
|
||||
userNotificationPreferencesUpdate(preferences: JSONObject!): Boolean
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
}
|
||||
|
||||
@@ -3,49 +3,49 @@ extend type Mutation {
|
||||
Invite a new user to the speckle server and return the invite ID
|
||||
"""
|
||||
serverInviteCreate(input: ServerInviteCreateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
"""
|
||||
Invite a new or registered user to the specified stream
|
||||
"""
|
||||
streamInviteCreate(input: StreamInviteCreateInput!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
serverInviteBatchCreate(input: [ServerInviteCreateInput!]!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
streamInviteBatchCreate(input: [StreamInviteCreateInput!]!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
"""
|
||||
Accept or decline a stream invite
|
||||
"""
|
||||
streamInviteUse(accept: Boolean!, streamId: String!, token: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
|
||||
"""
|
||||
Cancel a pending stream invite. Can only be invoked by a stream owner.
|
||||
"""
|
||||
streamInviteCancel(streamId: String!, inviteId: String!): Boolean!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
"""
|
||||
Re-send a pending invite
|
||||
"""
|
||||
inviteResend(inviteId: String!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "users:invite")
|
||||
|
||||
"""
|
||||
Delete a pending invite
|
||||
"""
|
||||
inviteDelete(inviteId: String!): Boolean!
|
||||
@hasRole(role: "server:admin")
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "users:invite")
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ extend type Query {
|
||||
Get all invitations to streams that the active user has
|
||||
"""
|
||||
streamInvites: [PendingStreamCollaborator!]!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "streams:read")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extend type Stream {
|
||||
webhooks(id: String): WebhookCollection
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
}
|
||||
|
||||
@@ -9,21 +9,21 @@ extend type Mutation {
|
||||
Creates a new webhook on a stream
|
||||
"""
|
||||
webhookCreate(webhook: WebhookCreateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
"""
|
||||
Updates an existing webhook
|
||||
"""
|
||||
webhookUpdate(webhook: WebhookUpdateInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
|
||||
"""
|
||||
Deletes an existing webhook
|
||||
"""
|
||||
webhookDelete(webhook: WebhookDeleteInput!): String!
|
||||
@hasRole(role: "server:user")
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "streams:write")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { saveActivity } from '@/modules/activitystream/services'
|
||||
import { ActionTypes, ResourceTypes } from '@/modules/activitystream/helpers/types'
|
||||
import { BranchRecord } from '@/modules/core/helpers/types'
|
||||
import { pubsub, BranchPubsubEvents } from '@/modules/shared'
|
||||
import {
|
||||
pubsub,
|
||||
BranchSubscriptions as BranchPubsubEvents
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
BranchDeleteInput,
|
||||
BranchUpdateInput,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
getViewerResourcesForComment,
|
||||
getViewerResourcesFromLegacyIdentifiers
|
||||
} from '@/modules/core/services/commit/viewerResources'
|
||||
import { pubsub } from '@/modules/shared'
|
||||
import { pubsub } from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
CommentSubscriptions,
|
||||
ProjectSubscriptions,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { saveActivity } from '@/modules/activitystream/services'
|
||||
import { ActionTypes, ResourceTypes } from '@/modules/activitystream/helpers/types'
|
||||
import { CommitPubsubEvents, pubsub } from '@/modules/shared'
|
||||
import {
|
||||
CommitSubscriptions as CommitPubsubEvents,
|
||||
pubsub
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
CommitCreateInput,
|
||||
CommitReceivedInput,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { saveActivity } from '@/modules/activitystream/services'
|
||||
import { ActionTypes, ResourceTypes } from '@/modules/activitystream/helpers/types'
|
||||
import { StreamRoles } from '@/modules/core/helpers/mainConstants'
|
||||
import { pubsub, StreamPubsubEvents } from '@/modules/shared'
|
||||
import {
|
||||
pubsub,
|
||||
StreamSubscriptions as StreamPubsubEvents
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { StreamCreateInput } from '@/test/graphql/generated/graphql'
|
||||
import { Knex } from 'knex'
|
||||
import { getStreamCollaborators } from '@/modules/core/repositories/streams'
|
||||
|
||||
@@ -11,7 +11,7 @@ const { noErrors } = require('@/test/helpers')
|
||||
const {
|
||||
addOrUpdateStreamCollaborator
|
||||
} = require('@/modules/core/services/streams/streamAccessService')
|
||||
const { Roles } = require('@/modules/core/helpers/mainConstants')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
|
||||
let sendRequest
|
||||
|
||||
@@ -79,14 +79,14 @@ describe('Activity @activity', () => {
|
||||
;({ sendRequest } = await initializeTestServer(server, app))
|
||||
|
||||
const normalScopesList = [
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
|
||||
// create users
|
||||
@@ -106,8 +106,8 @@ describe('Activity @activity', () => {
|
||||
(token) => (userCr.token = `Bearer ${token}`)
|
||||
),
|
||||
createPersonalAccessToken(userX.id, 'no users:read test token', [
|
||||
'streams:read',
|
||||
'streams:write'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write
|
||||
]).then((token) => (userX.token = `Bearer ${token}`))
|
||||
// streams
|
||||
// createStream({ ...collaboratorTestStream, ownerId: userIz.id }).then(
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
deleteApp,
|
||||
revokeExistingAppCredentialsForUser
|
||||
} = require('../../services/apps')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
@@ -56,10 +57,10 @@ module.exports = {
|
||||
async appUpdate(parent, args, context) {
|
||||
const app = await getApp({ id: args.app.id })
|
||||
// only admins can update the default apps, generated by the server
|
||||
if (!app.author && context.role !== 'server:admin')
|
||||
if (!app.author && context.role !== Roles.Server.Admin)
|
||||
throw new ForbiddenError('You are not authorized to edit this app.')
|
||||
// only the author or an admin can update a 3rd party app
|
||||
if (app.author.id !== context.userId && context.role !== 'server:admin')
|
||||
if (app.author.id !== context.userId && context.role !== Roles.Server.Admin)
|
||||
throw new ForbiddenError('You are not authorized to edit this app.')
|
||||
|
||||
await updateApp({ app: args.app })
|
||||
@@ -69,9 +70,9 @@ module.exports = {
|
||||
async appDelete(parent, args, context) {
|
||||
const app = await getApp({ id: args.appId })
|
||||
|
||||
if (!app.author && context.role !== 'server:admin')
|
||||
if (!app.author && context.role !== Roles.Server.Admin)
|
||||
throw new ForbiddenError('You are not authorized to edit this app.')
|
||||
if (app.author.id !== context.userId && context.role !== 'server:admin')
|
||||
if (app.author.id !== context.userId && context.role !== Roles.Server.Admin)
|
||||
throw new ForbiddenError('You are not authorized to edit this app.')
|
||||
|
||||
return (await deleteApp({ id: args.appId })) === 1
|
||||
|
||||
@@ -14,6 +14,7 @@ const { revokeRefreshToken } = require(`@/modules/auth/services/apps`)
|
||||
const { validateScopes } = require(`@/modules/shared`)
|
||||
const { InvalidAccessCodeRequestError } = require('@/modules/auth/errors')
|
||||
const { ForbiddenError } = require('apollo-server-errors')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
// TODO: Secure these endpoints!
|
||||
module.exports = (app) => {
|
||||
@@ -38,7 +39,7 @@ module.exports = (app) => {
|
||||
if (!valid) throw new InvalidAccessCodeRequestError('Invalid token')
|
||||
|
||||
// 2. Validate token scopes
|
||||
await validateScopes(scopes, 'tokens:write')
|
||||
await validateScopes(scopes, Scopes.Tokens.Write)
|
||||
|
||||
const ac = await createAuthorizationCode({ appId, userId, challenge })
|
||||
return res.redirect(`${app.redirectUrl}?access_code=${ac}`)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'apps:read',
|
||||
name: Scopes.Apps.Read,
|
||||
description: 'See what applications you have created or have authorized.',
|
||||
public: false
|
||||
},
|
||||
{
|
||||
name: 'apps:write',
|
||||
name: Scopes.Apps.Write,
|
||||
description: 'Register applications on your behalf.',
|
||||
public: false
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ module.exports = async (app, session, sessionStorage, finalizeAuth) => {
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: new URL(strategy.callbackUrl, process.env.CANONICAL_URL).toString(),
|
||||
// WARNING, the 'user:email' scope belongs to the GITHUB scopes
|
||||
// DO NOT change it to our internal scope definitions !!!
|
||||
// You have been warned.
|
||||
scope: ['profile', 'user:email'],
|
||||
passReqToCallback: true
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
createAuthorizationCode,
|
||||
createAppTokenFromAccessCode
|
||||
} = require('../services/apps')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
let sendRequest
|
||||
let server
|
||||
@@ -33,9 +34,9 @@ describe('GraphQL @apps-api', () => {
|
||||
|
||||
testUser.id = await createUser(testUser)
|
||||
testToken = `Bearer ${await createPersonalAccessToken(testUser.id, 'test token', [
|
||||
'profile:read',
|
||||
'apps:read',
|
||||
'apps:write'
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Apps.Read,
|
||||
Scopes.Apps.Write
|
||||
])}`
|
||||
|
||||
testUser2 = {
|
||||
@@ -46,9 +47,9 @@ describe('GraphQL @apps-api', () => {
|
||||
|
||||
testUser2.id = await createUser(testUser2)
|
||||
testToken2 = `Bearer ${await createPersonalAccessToken(testUser2.id, 'test token', [
|
||||
'profile:read',
|
||||
'apps:read',
|
||||
'apps:write'
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Apps.Read,
|
||||
Scopes.Apps.Write
|
||||
])}`
|
||||
})
|
||||
|
||||
@@ -67,7 +68,7 @@ describe('GraphQL @apps-api', () => {
|
||||
name: 'Test App',
|
||||
public: true,
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ describe('GraphQL @apps-api', () => {
|
||||
myApp: {
|
||||
name: 'Test App',
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
@@ -160,7 +161,7 @@ describe('GraphQL @apps-api', () => {
|
||||
id: testAppId,
|
||||
name: 'Updated Test App',
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
@@ -192,7 +193,7 @@ describe('GraphQL @apps-api', () => {
|
||||
name: 'Another Test App',
|
||||
public: false,
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
@@ -203,7 +204,7 @@ describe('GraphQL @apps-api', () => {
|
||||
name: 'The n-th Test App',
|
||||
public: false,
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { pubsub } = require('@/modules/shared')
|
||||
const { pubsub } = require('@/modules/shared/utils/subscriptions')
|
||||
const { ForbiddenError: ApolloForbiddenError } = require('apollo-server-express')
|
||||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const { getStream } = require('@/modules/core/services/streams')
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
markCommentViewed
|
||||
} = require('@/modules/comments/repositories/comments')
|
||||
const { clamp } = require('lodash')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
const Comments = () => knex('comments')
|
||||
const CommentLinks = () => knex('comment_links')
|
||||
@@ -221,7 +222,7 @@ module.exports = {
|
||||
.first()
|
||||
|
||||
if (comment.authorId !== userId) {
|
||||
if (!aclEntry || aclEntry.role !== 'stream:owner')
|
||||
if (!aclEntry || aclEntry.role !== Roles.Stream.Owner)
|
||||
throw new ForbiddenError("You don't have permission to archive the comment")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const { defaultFieldResolver } = require('graphql')
|
||||
const { validateServerRole, authorizeResolver } = require('@/modules/shared')
|
||||
const { authorizeResolver } = require('@/modules/shared')
|
||||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils')
|
||||
const {
|
||||
mapStreamRoleToValue,
|
||||
mapServerRoleToValue
|
||||
} = require('@/modules/core/helpers/graphTypes')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -19,6 +20,7 @@ module.exports = {
|
||||
enum ServerRole {
|
||||
SERVER_USER
|
||||
SERVER_ADMIN
|
||||
SERVER_GUEST
|
||||
SERVER_ARCHIVED_USER
|
||||
}
|
||||
|
||||
@@ -37,42 +39,10 @@ module.exports = {
|
||||
const { resolve = defaultFieldResolver } = fieldConfig
|
||||
fieldConfig.resolve = async function (...args) {
|
||||
const context = args[2]
|
||||
await validateServerRole(context, mapServerRoleToValue(requiredRole))
|
||||
|
||||
return await resolve.apply(this, args)
|
||||
}
|
||||
|
||||
return fieldConfig
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure that the user has the specified SERVER role (e.g. server user, admin etc.)
|
||||
* @deprecated Use `hasServerRole` instead, as it relies on proper GQL enums
|
||||
* @type {import('@/modules/core/graph/helpers/directiveHelper').GraphqlDirectiveBuilder}
|
||||
*/
|
||||
hasRole: () => {
|
||||
const directiveName = 'hasRole'
|
||||
return {
|
||||
typeDefs: `
|
||||
"""
|
||||
Ensure that the user has the specified SERVER role (e.g. server user, admin etc.)
|
||||
"""
|
||||
directive @${directiveName}(role: String!) on FIELD_DEFINITION
|
||||
`,
|
||||
schemaTransformer: (schema) =>
|
||||
mapSchema(schema, {
|
||||
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
|
||||
const directive = getDirective(schema, fieldConfig, directiveName)?.[0]
|
||||
if (!directive) return undefined
|
||||
|
||||
const { role: requiredRole } = directive
|
||||
const { resolve = defaultFieldResolver } = fieldConfig
|
||||
fieldConfig.resolve = async function (...args) {
|
||||
const context = args[2]
|
||||
await validateServerRole(context, requiredRole)
|
||||
await throwForNotHavingServerRole(
|
||||
context,
|
||||
mapServerRoleToValue(requiredRole)
|
||||
)
|
||||
|
||||
return await resolve.apply(this, args)
|
||||
}
|
||||
|
||||
@@ -1883,6 +1883,7 @@ export type ServerInfo = {
|
||||
canonicalUrl?: Maybe<Scalars['String']>;
|
||||
company?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
guestModeEnabled: Scalars['Boolean'];
|
||||
inviteOnly?: Maybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
roles: Array<Maybe<Role>>;
|
||||
@@ -1895,6 +1896,7 @@ export type ServerInfoUpdateInput = {
|
||||
adminContact?: InputMaybe<Scalars['String']>;
|
||||
company?: InputMaybe<Scalars['String']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
guestModeEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
inviteOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
termsOfService?: InputMaybe<Scalars['String']>;
|
||||
@@ -1915,6 +1917,7 @@ export type ServerInviteCreateInput = {
|
||||
export enum ServerRole {
|
||||
ServerAdmin = 'SERVER_ADMIN',
|
||||
ServerArchivedUser = 'SERVER_ARCHIVED_USER',
|
||||
ServerGuest = 'SERVER_GUEST',
|
||||
ServerUser = 'SERVER_USER'
|
||||
}
|
||||
|
||||
@@ -3059,12 +3062,6 @@ export type ResolversParentTypes = {
|
||||
WebhookUpdateInput: WebhookUpdateInput;
|
||||
};
|
||||
|
||||
export type HasRoleDirectiveArgs = {
|
||||
role: Scalars['String'];
|
||||
};
|
||||
|
||||
export type HasRoleDirectiveResolver<Result, Parent, ContextType = GraphQLContext, Args = HasRoleDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
|
||||
|
||||
export type HasScopeDirectiveArgs = {
|
||||
scope: Scalars['String'];
|
||||
};
|
||||
@@ -3722,6 +3719,7 @@ export type ServerInfoResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
canonicalUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
company?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
guestModeEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
inviteOnly?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
roles?: Resolver<Array<Maybe<ResolversTypes['Role']>>, ParentType, ContextType>;
|
||||
@@ -4067,7 +4065,6 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
};
|
||||
|
||||
export type DirectiveResolvers<ContextType = GraphQLContext> = {
|
||||
hasRole?: HasRoleDirectiveResolver<any, any, ContextType>;
|
||||
hasScope?: HasScopeDirectiveResolver<any, any, ContextType>;
|
||||
hasScopes?: HasScopesDirectiveResolver<any, any, ContextType>;
|
||||
hasServerRole?: HasServerRoleDirectiveResolver<any, any, ContextType>;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
const { withFilter } = require('graphql-subscriptions')
|
||||
|
||||
const { authorizeResolver, pubsub, BranchPubsubEvents } = require('@/modules/shared')
|
||||
const {
|
||||
pubsub,
|
||||
BranchSubscriptions: BranchPubsubEvents
|
||||
} = require('@/modules/shared/utils/subscriptions')
|
||||
const { authorizeResolver } = require('@/modules/shared')
|
||||
|
||||
const { getBranchByNameAndStreamId, getBranchById } = require('../../services/branches')
|
||||
const {
|
||||
@@ -15,6 +19,7 @@ const {
|
||||
} = require('@/modules/core/services/branch/retrieval')
|
||||
|
||||
const { getUserById } = require('../../services/users')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
// subscription events
|
||||
const BRANCH_CREATED = BranchPubsubEvents.BranchCreated
|
||||
@@ -62,7 +67,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.branch.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const { id } = await createBranchAndNotify(args.branch, context.userId)
|
||||
@@ -74,7 +79,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.branch.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const newBranch = await updateBranchAndNotify(args.branch, context.userId)
|
||||
@@ -85,7 +90,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.branch.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const deleted = await deleteBranchAndNotify(args.branch, context.userId)
|
||||
@@ -97,7 +102,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([BRANCH_CREATED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
@@ -108,7 +117,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([BRANCH_UPDATED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
const streamMatch = payload.streamId === variables.streamId
|
||||
if (streamMatch && variables.branchId) {
|
||||
@@ -124,7 +137,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([BRANCH_DELETED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
const { UserInputError, ApolloError } = require('apollo-server-express')
|
||||
const { withFilter } = require('graphql-subscriptions')
|
||||
const { authorizeResolver, pubsub, CommitPubsubEvents } = require('@/modules/shared')
|
||||
const {
|
||||
pubsub,
|
||||
CommitSubscriptions: CommitPubsubEvents
|
||||
} = require('@/modules/shared/utils/subscriptions')
|
||||
const { authorizeResolver } = require('@/modules/shared')
|
||||
|
||||
const {
|
||||
getCommitById,
|
||||
@@ -39,6 +43,7 @@ const {
|
||||
validateStreamAccess
|
||||
} = require('@/modules/core/services/streams/streamAccessService')
|
||||
const { StreamInvalidAccessError } = require('@/modules/core/errors/stream')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
// subscription events
|
||||
const COMMIT_CREATED = CommitPubsubEvents.CommitCreated
|
||||
@@ -167,7 +172,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.commit.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const rateLimitResult = await getRateLimitResult(
|
||||
@@ -190,7 +195,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.commit.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
await updateCommitAndNotify(args.commit, context.userId)
|
||||
@@ -198,7 +203,11 @@ module.exports = {
|
||||
},
|
||||
|
||||
async commitReceive(parent, args, context) {
|
||||
await authorizeResolver(context.userId, args.input.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.input.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
const commit = await getCommitById({
|
||||
streamId: args.input.streamId,
|
||||
@@ -218,7 +227,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.commit.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const deleted = await deleteCommitAndNotify(
|
||||
@@ -244,7 +253,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([COMMIT_CREATED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
)
|
||||
@@ -254,7 +267,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([COMMIT_UPDATED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
const streamMatch = payload.streamId === variables.streamId
|
||||
if (streamMatch && variables.commitId) {
|
||||
@@ -270,7 +287,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([COMMIT_DELETED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
'use strict'
|
||||
const {
|
||||
validateServerRole,
|
||||
validateScopes,
|
||||
authorizeResolver
|
||||
} = require('@/modules/shared')
|
||||
const { validateScopes, authorizeResolver } = require('@/modules/shared')
|
||||
|
||||
const {
|
||||
createObjects,
|
||||
@@ -11,6 +7,8 @@ const {
|
||||
getObjectChildren,
|
||||
getObjectChildrenQuery
|
||||
} = require('../../services/objects')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
module.exports = {
|
||||
Stream: {
|
||||
@@ -59,12 +57,12 @@ module.exports = {
|
||||
},
|
||||
Mutation: {
|
||||
async objectCreate(parent, args, context) {
|
||||
await validateServerRole(context, 'server:user')
|
||||
await validateScopes(context.scopes, 'streams:write')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await validateScopes(context.scopes, Scopes.Streams.Write)
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.objectInput.streamId,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
|
||||
const ids = await createObjects(
|
||||
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
createStreamInviteAndNotify,
|
||||
useStreamInviteAndNotify
|
||||
} from '@/modules/serverinvites/services/management'
|
||||
import { authorizeResolver, validateScopes, validateServerRole } from '@/modules/shared'
|
||||
import { authorizeResolver, validateScopes } from '@/modules/shared'
|
||||
import { throwForNotHavingServerRole } from '@/modules/shared/authz'
|
||||
import {
|
||||
filteredSubscribe,
|
||||
ProjectSubscriptions,
|
||||
@@ -52,7 +53,7 @@ export = {
|
||||
await authorizeResolver(context.userId, args.id, Roles.Stream.Reviewer)
|
||||
|
||||
if (!stream.isPublic) {
|
||||
await validateServerRole(context, Roles.Server.User)
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
validateScopes(context.scopes, Scopes.Streams.Read)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use strict'
|
||||
const { validateServerRole, validateScopes } = require('@/modules/shared')
|
||||
const { validateScopes } = require('@/modules/shared')
|
||||
const {
|
||||
updateServerInfo,
|
||||
getServerInfo,
|
||||
getPublicScopes,
|
||||
getPublicRoles
|
||||
} = require('../../services/generic')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
@@ -26,8 +28,8 @@ module.exports = {
|
||||
|
||||
Mutation: {
|
||||
async serverInfoUpdate(parent, args, context) {
|
||||
await validateServerRole(context, 'server:admin')
|
||||
await validateScopes(context.scopes, 'server:setup')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Admin)
|
||||
await validateScopes(context.scopes, Scopes.Server.Setup)
|
||||
|
||||
await updateServerInfo(args.info)
|
||||
return true
|
||||
|
||||
@@ -14,12 +14,11 @@ const {
|
||||
} = require('@/modules/core/services/streams')
|
||||
|
||||
const {
|
||||
authorizeResolver,
|
||||
pubsub,
|
||||
StreamPubsubEvents,
|
||||
validateScopes,
|
||||
validateServerRole
|
||||
} = require(`@/modules/shared`)
|
||||
StreamSubscriptions: StreamPubsubEvents
|
||||
} = require(`@/modules/shared/utils/subscriptions`)
|
||||
|
||||
const { authorizeResolver, validateScopes } = require(`@/modules/shared`)
|
||||
const {
|
||||
RateLimitError,
|
||||
RateLimitAction,
|
||||
@@ -48,8 +47,9 @@ const {
|
||||
updateStreamRoleAndNotify
|
||||
} = require('@/modules/core/services/streams/management')
|
||||
const { adminOverrideEnabled } = require('@/modules/shared/helpers/envHelper')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { StreamNotFoundError } = require('@/modules/core/errors/stream')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
// subscription events
|
||||
const USER_STREAM_ADDED = StreamPubsubEvents.UserStreamAdded
|
||||
@@ -85,11 +85,11 @@ module.exports = {
|
||||
throw new StreamNotFoundError('Stream not found')
|
||||
}
|
||||
|
||||
await authorizeResolver(context.userId, args.id, 'stream:reviewer')
|
||||
await authorizeResolver(context.userId, args.id, Roles.Stream.Reviewer)
|
||||
|
||||
if (!stream.isPublic) {
|
||||
await validateServerRole(context, 'server:user')
|
||||
await validateScopes(context.scopes, 'streams:read')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await validateScopes(context.scopes, Scopes.Streams.Read)
|
||||
}
|
||||
|
||||
return stream
|
||||
@@ -221,13 +221,13 @@ module.exports = {
|
||||
},
|
||||
|
||||
async streamUpdate(parent, args, context) {
|
||||
await authorizeResolver(context.userId, args.stream.id, 'stream:owner')
|
||||
await authorizeResolver(context.userId, args.stream.id, Roles.Stream.Owner)
|
||||
await updateStreamAndNotify(args.stream, context.userId)
|
||||
return true
|
||||
},
|
||||
|
||||
async streamDelete(parent, args, context, info) {
|
||||
await authorizeResolver(context.userId, args.id, 'stream:owner')
|
||||
await authorizeResolver(context.userId, args.id, Roles.Stream.Owner)
|
||||
return await _deleteStream(parent, args, context, info)
|
||||
},
|
||||
|
||||
@@ -246,7 +246,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.permissionParams.streamId,
|
||||
'stream:owner'
|
||||
Roles.Stream.Owner
|
||||
)
|
||||
|
||||
const result = await updateStreamRoleAndNotify(
|
||||
@@ -260,7 +260,7 @@ module.exports = {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.permissionParams.streamId,
|
||||
'stream:owner'
|
||||
Roles.Stream.Owner
|
||||
)
|
||||
|
||||
const result = await updateStreamRoleAndNotify(
|
||||
@@ -310,7 +310,7 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([STREAM_UPDATED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.id, 'stream:reviewer')
|
||||
await authorizeResolver(context.userId, payload.id, Roles.Stream.Reviewer)
|
||||
return payload.id === variables.streamId
|
||||
}
|
||||
)
|
||||
@@ -320,7 +320,11 @@ module.exports = {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator([STREAM_DELETED]),
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,21 +6,21 @@ const {
|
||||
getUserRole,
|
||||
deleteUser,
|
||||
searchUsers,
|
||||
makeUserAdmin,
|
||||
unmakeUserAdmin,
|
||||
archiveUser
|
||||
} = require('../../services/users')
|
||||
changeUserRole
|
||||
} = require('@/modules/core/services/users')
|
||||
const { updateUserAndNotify } = require('@/modules/core/services/users/management')
|
||||
const { saveActivity } = require('@/modules/activitystream/services')
|
||||
const { ActionTypes } = require('@/modules/activitystream/helpers/types')
|
||||
const { validateServerRole, validateScopes } = require(`@/modules/shared`)
|
||||
const { validateScopes } = require(`@/modules/shared`)
|
||||
const zxcvbn = require('zxcvbn')
|
||||
const {
|
||||
getAdminUsersListCollection
|
||||
} = require('@/modules/core/services/users/adminUsersListService')
|
||||
const { Roles, Scopes } = require('@/modules/core/helpers/mainConstants')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { markOnboardingComplete } = require('@/modules/core/repositories/users')
|
||||
const { UsersMeta } = require('@/modules/core/dbSchema')
|
||||
const { getServerInfo } = require('@/modules/core/services/generic')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
/** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */
|
||||
module.exports = {
|
||||
@@ -33,8 +33,8 @@ module.exports = {
|
||||
if (!activeUserId) return null
|
||||
|
||||
// Only if authenticated - check for server roles & scopes
|
||||
await validateServerRole(context, 'server:user')
|
||||
await validateScopes(context.scopes, 'profile:read')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await validateScopes(context.scopes, Scopes.Profile.Read)
|
||||
|
||||
return await getUser(activeUserId)
|
||||
},
|
||||
@@ -47,10 +47,10 @@ module.exports = {
|
||||
// User wants info about himself and he's not authenticated - just return null
|
||||
if (!context.auth && !args.id) return null
|
||||
|
||||
await validateServerRole(context, 'server:user')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
|
||||
if (!args.id) await validateScopes(context.scopes, 'profile:read')
|
||||
else await validateScopes(context.scopes, 'users:read')
|
||||
if (!args.id) await validateScopes(context.scopes, Scopes.Profile.Read)
|
||||
else await validateScopes(context.scopes, Scopes.Users.Read)
|
||||
|
||||
if (!args.id && !context.userId) {
|
||||
throw new UserInputError('You must provide an user id.')
|
||||
@@ -64,9 +64,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
async userSearch(parent, args, context) {
|
||||
await validateServerRole(context, 'server:user')
|
||||
await validateScopes(context.scopes, 'profile:read')
|
||||
await validateScopes(context.scopes, 'users:read')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await validateScopes(context.scopes, Scopes.Profile.Read)
|
||||
await validateScopes(context.scopes, Scopes.Users.Read)
|
||||
|
||||
if (args.query.length < 3)
|
||||
throw new UserInputError('Search query must be at least 3 carachters.')
|
||||
@@ -97,7 +97,7 @@ module.exports = {
|
||||
// NOTE: we're redacting the field (returning null) rather than throwing a full error which would invalidate the request.
|
||||
if (context.userId === parent.id) {
|
||||
try {
|
||||
await validateScopes(context.scopes, 'profile:email')
|
||||
await validateScopes(context.scopes, Scopes.Profile.Email)
|
||||
return parent.email
|
||||
} catch (err) {
|
||||
return null
|
||||
@@ -106,7 +106,7 @@ module.exports = {
|
||||
|
||||
try {
|
||||
// you should only have access to other users email if you have elevated privileges
|
||||
await validateServerRole(context, Roles.Server.Admin)
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Admin)
|
||||
await validateScopes(context.scopes, Scopes.Users.Email)
|
||||
return parent.email
|
||||
} catch (err) {
|
||||
@@ -130,25 +130,24 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
async userUpdate(parent, args, context) {
|
||||
await validateServerRole(context, 'server:user')
|
||||
async userUpdate(_parent, args, context) {
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await updateUserAndNotify(context.userId, args.user)
|
||||
return true
|
||||
},
|
||||
|
||||
async userRoleChange(parent, args) {
|
||||
const roleChangers = {
|
||||
'server:admin': makeUserAdmin,
|
||||
'server:user': unmakeUserAdmin,
|
||||
'server:archived-user': archiveUser
|
||||
}
|
||||
const roleChanger = roleChangers[args.userRoleInput.role]
|
||||
await roleChanger({ userId: args.userRoleInput.id })
|
||||
async userRoleChange(_parent, args) {
|
||||
const { guestModeEnabled } = await getServerInfo()
|
||||
await changeUserRole({
|
||||
role: args.userRoleInput.role,
|
||||
userId: args.userRoleInput.id,
|
||||
guestModeEnabled
|
||||
})
|
||||
return true
|
||||
},
|
||||
|
||||
async adminDeleteUser(parent, args, context) {
|
||||
await validateServerRole(context, 'server:admin')
|
||||
async adminDeleteUser(_parent, args, context) {
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Admin)
|
||||
const user = await getUserByEmail({ email: args.userConfirmation.email })
|
||||
await deleteUser(user.id)
|
||||
return true
|
||||
@@ -164,8 +163,8 @@ module.exports = {
|
||||
// The below are not really needed anymore as we've added the hasRole and hasScope
|
||||
// directives in the graphql schema itself.
|
||||
// Since I am paranoid, I'll leave them here too.
|
||||
await validateServerRole(context, 'server:user')
|
||||
await validateScopes(context.scopes, 'profile:delete')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
await validateScopes(context.scopes, Scopes.Profile.Delete)
|
||||
|
||||
await deleteUser(context.userId, args.user)
|
||||
|
||||
|
||||
@@ -127,5 +127,7 @@ export function mapServerRoleToValue(graphqlServerRole: ServerRole): ServerRoles
|
||||
return Roles.Server.Admin
|
||||
case ServerRole.ServerArchivedUser:
|
||||
return Roles.Server.ArchivedUser
|
||||
case ServerRole.ServerGuest:
|
||||
return Roles.Server.Guest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export type ServerConfigRecord = {
|
||||
canonicalUrl: string
|
||||
completed: boolean
|
||||
inviteOnly: boolean
|
||||
guestModeEnabled: boolean
|
||||
}
|
||||
|
||||
export type ServerInfo = ServerConfigRecord & {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
const TABLE_NAME = 'server_config'
|
||||
const COL_NAME = 'guestModeEnabled'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TABLE_NAME, (table) => {
|
||||
table.boolean(COL_NAME).defaultTo(false).notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TABLE_NAME, (table) => {
|
||||
table.dropColumn(COL_NAME)
|
||||
})
|
||||
}
|
||||
@@ -930,7 +930,7 @@ export async function revokeStreamPermissions(params: {
|
||||
.select<StreamAclRecord[]>('*')
|
||||
.first()
|
||||
|
||||
if (aclEntry?.role === 'stream:owner') {
|
||||
if (aclEntry?.role === Roles.Stream.Owner) {
|
||||
const [countObj] = await StreamAcl.knex()
|
||||
.where({
|
||||
resourceId: streamId,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use strict'
|
||||
const {
|
||||
validateScopes,
|
||||
validateServerRole,
|
||||
authorizeResolver
|
||||
} = require('@/modules/shared')
|
||||
const { validateScopes, authorizeResolver } = require('@/modules/shared')
|
||||
|
||||
const { getStream } = require('../services/streams')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
module.exports = {
|
||||
async validatePermissionsReadStream(streamId, req) {
|
||||
@@ -13,7 +11,7 @@ module.exports = {
|
||||
if (stream?.isPublic) return { result: true, status: 200 }
|
||||
|
||||
try {
|
||||
await validateServerRole(req.context, 'server:user')
|
||||
await throwForNotHavingServerRole(req.context, Roles.Server.Guest)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
@@ -26,13 +24,13 @@ module.exports = {
|
||||
|
||||
if (!stream.isPublic) {
|
||||
try {
|
||||
await validateScopes(req.context.scopes, 'streams:read')
|
||||
await validateScopes(req.context.scopes, Scopes.Streams.Read)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
|
||||
try {
|
||||
await authorizeResolver(req.context.userId, streamId, 'stream:reviewer')
|
||||
await authorizeResolver(req.context.userId, streamId, Roles.Stream.Reviewer)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
@@ -46,19 +44,19 @@ module.exports = {
|
||||
}
|
||||
|
||||
try {
|
||||
await validateServerRole(req.context, 'server:user')
|
||||
await throwForNotHavingServerRole(req.context, Roles.Server.Guest)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
|
||||
try {
|
||||
await validateScopes(req.context.scopes, 'streams:write')
|
||||
await validateScopes(req.context.scopes, Scopes.Streams.Write)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
|
||||
try {
|
||||
await authorizeResolver(req.context.userId, streamId, 'stream:contributor')
|
||||
await authorizeResolver(req.context.userId, streamId, Roles.Stream.Contributor)
|
||||
} catch (err) {
|
||||
return { result: false, status: 401 }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,18 @@ module.exports = [
|
||||
weight: 100,
|
||||
public: false
|
||||
},
|
||||
// TODO: should this be dynamically pushed if guest role is enabled?
|
||||
// feels risky, since feature can be toggled on and off,
|
||||
// but user roles are not updated
|
||||
// can leave the guest users in a broken state
|
||||
{
|
||||
name: Roles.Server.Guest,
|
||||
description: 'Has limited access to the server.',
|
||||
resourceTarget: 'server',
|
||||
aclTableName: 'server_acl',
|
||||
weight: 50,
|
||||
public: false
|
||||
},
|
||||
{
|
||||
name: Roles.Server.ArchivedUser,
|
||||
description: 'No longer has access to the server.',
|
||||
|
||||
@@ -38,7 +38,8 @@ module.exports = {
|
||||
description,
|
||||
adminContact,
|
||||
termsOfService,
|
||||
inviteOnly
|
||||
inviteOnly,
|
||||
guestModeEnabled
|
||||
}) {
|
||||
const serverInfo = await Info().select('*').first()
|
||||
if (!serverInfo)
|
||||
@@ -49,6 +50,7 @@ module.exports = {
|
||||
adminContact,
|
||||
termsOfService,
|
||||
inviteOnly,
|
||||
guestModeEnabled,
|
||||
completed: true
|
||||
})
|
||||
else
|
||||
@@ -59,6 +61,7 @@ module.exports = {
|
||||
adminContact,
|
||||
termsOfService,
|
||||
inviteOnly,
|
||||
guestModeEnabled,
|
||||
completed: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const { authorizeResolver } = require(`@/modules/shared`)
|
||||
|
||||
const { Roles } = require('@/modules/core/helpers/mainConstants')
|
||||
const { LogicError } = require('@/modules/shared/errors')
|
||||
const { ForbiddenError } = require('apollo-server-express')
|
||||
const { ForbiddenError, UserInputError } = require('apollo-server-express')
|
||||
const { StreamInvalidAccessError } = require('@/modules/core/errors/stream')
|
||||
const {
|
||||
addStreamPermissionsAddedActivity,
|
||||
@@ -15,6 +15,8 @@ const {
|
||||
grantStreamPermissions
|
||||
} = require('@/modules/core/repositories/streams')
|
||||
|
||||
const { ServerAcl } = require('@/modules/core/dbSchema')
|
||||
|
||||
/**
|
||||
* Check if user is a stream collaborator
|
||||
* @param {string} userId
|
||||
@@ -129,6 +131,13 @@ async function addOrUpdateStreamCollaborator(
|
||||
|
||||
await validateStreamAccess(addedById, streamId, Roles.Stream.Owner)
|
||||
|
||||
// make sure server guests cannot be stream owners
|
||||
if (role === Roles.Stream.Owner) {
|
||||
const userServerRole = await ServerAcl.knex().where({ userId }).first()
|
||||
if (userServerRole.role === Roles.Server.Guest)
|
||||
throw new UserInputError('Server guests cannot own streams')
|
||||
}
|
||||
|
||||
const stream = await grantStreamPermissions({
|
||||
streamId,
|
||||
userId,
|
||||
|
||||
@@ -26,17 +26,18 @@ const {
|
||||
UserInputError,
|
||||
PasswordTooShortError
|
||||
} = require('@/modules/core/errors/userinput')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
const changeUserRole = async ({ userId, role }) =>
|
||||
const _changeUserRole = async ({ userId, role }) =>
|
||||
await Acl().where({ userId }).update({ role })
|
||||
|
||||
const countAdminUsers = async () => {
|
||||
const [{ count }] = await Acl().where({ role: 'server:admin' }).count()
|
||||
const [{ count }] = await Acl().where({ role: Roles.Server.Admin }).count()
|
||||
return parseInt(count)
|
||||
}
|
||||
const _ensureAtleastOneAdminRemains = async (userId) => {
|
||||
if ((await countAdminUsers()) === 1) {
|
||||
const currentAdmin = await Acl().where({ role: 'server:admin' }).first()
|
||||
const currentAdmin = await Acl().where({ role: Roles.Server.Admin }).first()
|
||||
if (currentAdmin.userId === userId) {
|
||||
throw new UserInputError('Cannot remove the last admin role from the server')
|
||||
}
|
||||
@@ -93,7 +94,8 @@ module.exports = {
|
||||
const [newUser] = (await Users().insert(user, UsersSchema.cols)) || []
|
||||
if (!newUser) throw new Error("Couldn't create user")
|
||||
|
||||
const userRole = (await countAdminUsers()) === 0 ? 'server:admin' : 'server:user'
|
||||
const userRole =
|
||||
(await countAdminUsers()) === 0 ? Roles.Server.Admin : Roles.Server.User
|
||||
|
||||
await Acl().insert({ userId: newId, role: userRole })
|
||||
|
||||
@@ -182,7 +184,7 @@ module.exports = {
|
||||
.where((queryBuilder) => {
|
||||
queryBuilder.where({ email: searchQuery }) //match full email or partial name
|
||||
if (!emailOnly) queryBuilder.orWhere('name', 'ILIKE', `%${searchQuery}%`)
|
||||
if (!archived) queryBuilder.andWhere('role', '!=', 'server:archived-user')
|
||||
if (!archived) queryBuilder.andWhere('role', '!=', Roles.Server.ArchivedUser)
|
||||
})
|
||||
|
||||
if (cursor) query.andWhere('users.createdAt', '<', cursor)
|
||||
@@ -225,9 +227,9 @@ module.exports = {
|
||||
(
|
||||
-- Get streams ids on which the user is owner
|
||||
SELECT "resourceId" FROM stream_acl
|
||||
WHERE role = 'stream:owner' AND "userId" = ?
|
||||
WHERE role = '${Roles.Stream.Owner}' AND "userId" = ?
|
||||
) AS us ON acl."resourceId" = us."resourceId"
|
||||
WHERE acl.role = 'stream:owner'
|
||||
WHERE acl.role = '${Roles.Stream.Owner}'
|
||||
GROUP BY (acl."resourceId")
|
||||
) AS soc
|
||||
WHERE cnt = 1
|
||||
@@ -262,25 +264,18 @@ module.exports = {
|
||||
return users
|
||||
},
|
||||
|
||||
async makeUserAdmin({ userId }) {
|
||||
await changeUserRole({ userId, role: 'server:admin' })
|
||||
},
|
||||
|
||||
async unmakeUserAdmin({ userId }) {
|
||||
// dont delete last admin role
|
||||
await _ensureAtleastOneAdminRemains(userId)
|
||||
await changeUserRole({ userId, role: 'server:user' })
|
||||
},
|
||||
|
||||
async archiveUser({ userId }) {
|
||||
// dont change last admin to archived
|
||||
await _ensureAtleastOneAdminRemains(userId)
|
||||
await changeUserRole({ userId, role: 'server:archived-user' })
|
||||
},
|
||||
|
||||
async countUsers(searchQuery = null) {
|
||||
const query = getUsersBaseQuery(searchQuery)
|
||||
const [userCount] = await query.count()
|
||||
return parseInt(userCount.count)
|
||||
},
|
||||
|
||||
async changeUserRole({ userId, role, guestModeEnabled = false }) {
|
||||
if (!Object.values(Roles.Server).includes(role))
|
||||
throw new UserInputError(`Invalid role specified: ${role}`)
|
||||
if (!guestModeEnabled && role === Roles.Server.Guest)
|
||||
throw new UserInputError('Guest role is not enabled')
|
||||
if (role !== Roles.Server.Admin) await _ensureAtleastOneAdminRemains(userId)
|
||||
await _changeUserRole({ userId, role })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ 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('must provide an auth token')
|
||||
expect(result.errors.at(0).message).to.contain('Must provide an auth token')
|
||||
})
|
||||
|
||||
it("can't be retrieved", async () => {
|
||||
|
||||
@@ -13,13 +13,11 @@ const { beforeEachContext } = require('@/test/hooks')
|
||||
const { createStream } = require('@/modules/core/services/streams')
|
||||
const { createUser } = require('@/modules/core/services/users')
|
||||
|
||||
const {
|
||||
validateServerRole,
|
||||
validateScopes,
|
||||
authorizeResolver
|
||||
} = require('@/modules/shared')
|
||||
const { validateScopes, authorizeResolver } = require('@/modules/shared')
|
||||
const { buildContext } = require('@/modules/shared/middleware')
|
||||
const { ForbiddenError } = require('apollo-server-express')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
before(async () => {
|
||||
@@ -60,7 +58,10 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
)
|
||||
|
||||
it('Should validate server role', async () => {
|
||||
await validateServerRole({ auth: true, role: 'server:user' }, 'server:admin')
|
||||
await throwForNotHavingServerRole(
|
||||
{ auth: true, role: Roles.Server.User },
|
||||
Roles.Server.Admin
|
||||
)
|
||||
.then(() => {
|
||||
throw new Error('This should have been rejected')
|
||||
})
|
||||
@@ -68,21 +69,28 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
expect('You do not have the required server role').to.equal(err.message)
|
||||
)
|
||||
|
||||
await validateServerRole({ auth: true, role: 'HACZOR' }, '133TCR3w')
|
||||
await throwForNotHavingServerRole({ auth: true, role: 'HACZOR' }, '133TCR3w')
|
||||
.then(() => {
|
||||
throw new Error('This should have been rejected')
|
||||
})
|
||||
.catch((err) => expect('Invalid server role specified').to.equal(err.message))
|
||||
.catch((err) =>
|
||||
expect('Invalid role requirement specified').to.equal(err.message)
|
||||
)
|
||||
|
||||
await validateServerRole({ auth: true, role: 'server:admin' }, '133TCR3w')
|
||||
await throwForNotHavingServerRole(
|
||||
{ auth: true, role: Roles.Server.Admin },
|
||||
'133TCR3w'
|
||||
)
|
||||
.then(() => {
|
||||
throw new Error('This should have been rejected')
|
||||
})
|
||||
.catch((err) => expect('Invalid server role specified').to.equal(err.message))
|
||||
.catch((err) =>
|
||||
expect('Invalid role requirement specified').to.equal(err.message)
|
||||
)
|
||||
|
||||
const test = await validateServerRole(
|
||||
{ auth: true, role: 'server:admin' },
|
||||
'server:user'
|
||||
const test = await throwForNotHavingServerRole(
|
||||
{ auth: true, role: Roles.Server.Admin },
|
||||
Roles.Server.User
|
||||
)
|
||||
expect(test).to.equal(true)
|
||||
})
|
||||
@@ -95,7 +103,7 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
.catch((err) => expect('Unknown role: bar').to.equal(err.message))
|
||||
|
||||
// this caught me out, but streams:read is not a valid role for now
|
||||
await authorizeResolver('foo', 'bar', 'streams:read')
|
||||
await authorizeResolver('foo', 'bar', Scopes.Streams.Read)
|
||||
.then(() => {
|
||||
throw new Error('This should have been rejected')
|
||||
})
|
||||
@@ -148,9 +156,9 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
myStream.id,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
expect(role).to.equal('stream:owner')
|
||||
expect(role).to.equal(Roles.Stream.Owner)
|
||||
})
|
||||
|
||||
it('should get the passed in role for server:admins if override enabled', async () => {
|
||||
@@ -159,13 +167,17 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
myStream.id,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
expect(role).to.equal('stream:contributor')
|
||||
expect(role).to.equal(Roles.Stream.Contributor)
|
||||
})
|
||||
it('should not allow server:admins to be anything if adminOverride is disabled', async () => {
|
||||
try {
|
||||
await authorizeResolver(serverOwner.id, notMyStream.id, 'stream:contributor')
|
||||
await authorizeResolver(
|
||||
serverOwner.id,
|
||||
notMyStream.id,
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
@@ -179,14 +191,14 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
const role = await authorizeResolver(
|
||||
serverOwner.id,
|
||||
notMyStream.id,
|
||||
'stream:contributor'
|
||||
Roles.Stream.Contributor
|
||||
)
|
||||
expect(role).to.equal('stream:contributor')
|
||||
expect(role).to.equal(Roles.Stream.Contributor)
|
||||
})
|
||||
|
||||
it('should not allow server:users to be anything if adminOverride is disabled', async () => {
|
||||
try {
|
||||
await authorizeResolver(otherGuy.id, myStream.id, 'stream:contributor')
|
||||
await authorizeResolver(otherGuy.id, myStream.id, Roles.Stream.Contributor)
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
@@ -197,7 +209,7 @@ describe('Generic AuthN & AuthZ controller tests', () => {
|
||||
envHelperMock.enable()
|
||||
envHelperMock.mockFunction('adminOverrideEnabled', () => true)
|
||||
try {
|
||||
await authorizeResolver(otherGuy.id, myStream.id, 'stream:contributor')
|
||||
await authorizeResolver(otherGuy.id, myStream.id, Roles.Stream.Contributor)
|
||||
throw 'This should have thrown'
|
||||
} catch (e) {
|
||||
expect(e instanceof ForbiddenError)
|
||||
|
||||
@@ -5,13 +5,17 @@ const request = require('supertest')
|
||||
const { beforeEachContext, initializeTestServer } = require(`@/test/hooks`)
|
||||
const { generateManyObjects } = require(`@/test/helpers`)
|
||||
|
||||
const { createUser, getUsers, archiveUser } = require('../services/users')
|
||||
const {
|
||||
createUser,
|
||||
getUsers,
|
||||
changeUserRole
|
||||
} = require('@/modules/core/services/users')
|
||||
const { createPersonalAccessToken } = require('../services/tokens')
|
||||
const {
|
||||
addOrUpdateStreamCollaborator,
|
||||
removeStreamCollaborator
|
||||
} = require('@/modules/core/services/streams/streamAccessService')
|
||||
const { Roles } = require('@/modules/core/helpers/mainConstants')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
|
||||
let app
|
||||
let server
|
||||
@@ -44,15 +48,15 @@ describe('GraphQL API Core @core-api', () => {
|
||||
userA.id,
|
||||
'test token user A',
|
||||
[
|
||||
'server:setup',
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Server.Setup,
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
userB.id = await createUser(userB)
|
||||
@@ -60,14 +64,14 @@ describe('GraphQL API Core @core-api', () => {
|
||||
userB.id,
|
||||
'test token user B',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
userC.id = await createUser(userC)
|
||||
@@ -75,14 +79,14 @@ describe('GraphQL API Core @core-api', () => {
|
||||
userC.id,
|
||||
'test token user B',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
@@ -243,14 +247,14 @@ describe('GraphQL API Core @core-api', () => {
|
||||
userDelete.id,
|
||||
'fail token user del',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
@@ -271,15 +275,15 @@ describe('GraphQL API Core @core-api', () => {
|
||||
userDelete.id,
|
||||
'test token user del',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email',
|
||||
'profile:delete'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email,
|
||||
Scopes.Profile.Delete
|
||||
]
|
||||
)}`
|
||||
|
||||
@@ -303,30 +307,30 @@ describe('GraphQL API Core @core-api', () => {
|
||||
let queriedUserB = await sendRequest(userA.token, {
|
||||
query: ` { otherUser(id:"${userB.id}") { id name role } }`
|
||||
})
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal('server:user')
|
||||
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.User)
|
||||
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.Admin}"})}`
|
||||
await sendRequest(userA.token, { query })
|
||||
queriedUserB = await sendRequest(userA.token, {
|
||||
query: ` { otherUser(id:"${userB.id}") { id name role } }`
|
||||
})
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal('server:admin')
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.Admin)
|
||||
expect(queriedUserB.body.data)
|
||||
query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:user"})}`
|
||||
query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.User}"})}`
|
||||
await sendRequest(userA.token, { query })
|
||||
queriedUserB = await sendRequest(userA.token, {
|
||||
query: ` { otherUser(id:"${userB.id}") { id name role } }`
|
||||
})
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal('server:user')
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.User)
|
||||
})
|
||||
|
||||
it('Only admins can change user role', async () => {
|
||||
const query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
|
||||
const query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.Admin}"})}`
|
||||
const res = await sendRequest(userB.token, { query })
|
||||
const queriedUserB = await sendRequest(userA.token, {
|
||||
query: ` { otherUser(id:"${userB.id}") { id name role } }`
|
||||
})
|
||||
expect(res.body.errors).to.exist
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal('server:user')
|
||||
expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.User)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1049,7 +1053,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
expect(res.body.data).to.have.property('user')
|
||||
expect(res.body.data.user.name).to.equal('Miticå')
|
||||
expect(res.body.data.user.email).to.equal('d.1@speckle.systems')
|
||||
expect(res.body.data.user.role).to.equal('server:admin')
|
||||
expect(res.body.data.user.role).to.equal(Roles.Server.Admin)
|
||||
})
|
||||
|
||||
it('Should retrieve my streams', async () => {
|
||||
@@ -1271,8 +1275,8 @@ describe('GraphQL API Core @core-api', () => {
|
||||
|
||||
expect(stream.name).to.equal('TS1 (u A) Private UPDATED')
|
||||
expect(stream.collaborators).to.have.lengthOf(2)
|
||||
expect(stream.collaborators[0].role).to.equal('stream:contributor')
|
||||
expect(stream.collaborators[1].role).to.equal('stream:owner')
|
||||
expect(stream.collaborators[0].role).to.equal(Roles.Stream.Contributor)
|
||||
expect(stream.collaborators[1].role).to.equal(Roles.Stream.Owner)
|
||||
})
|
||||
|
||||
it('Should retrieve a public stream even if not authenticated', async () => {
|
||||
@@ -1688,20 +1692,20 @@ describe('GraphQL API Core @core-api', () => {
|
||||
archivedUser.id,
|
||||
'this will be archived',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email',
|
||||
'apps:read',
|
||||
'apps:write',
|
||||
'users:invite'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email,
|
||||
Scopes.Apps.Read,
|
||||
Scopes.Apps.Write,
|
||||
Scopes.Users.Invite
|
||||
]
|
||||
)}`
|
||||
await archiveUser({ userId: archivedUser.id })
|
||||
await changeUserRole({ userId: archivedUser.id, role: Roles.Server.ArchivedUser })
|
||||
})
|
||||
|
||||
it('Should be able to read public streams', async () => {
|
||||
@@ -1730,7 +1734,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
query,
|
||||
variables: {
|
||||
tokenInput: {
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
name: 'thisWillNotBeCreated',
|
||||
lifespan: 1000000
|
||||
}
|
||||
@@ -1835,7 +1839,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
name: 'Test App',
|
||||
public: true,
|
||||
description: 'Test App Description',
|
||||
scopes: ['streams:read'],
|
||||
scopes: [Scopes.Streams.Read],
|
||||
redirectUrl: 'lol://what'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const { packageRoot } = require('@/bootstrap')
|
||||
const {
|
||||
addOrUpdateStreamCollaborator
|
||||
} = require('@/modules/core/services/streams/streamAccessService')
|
||||
const { Roles } = require('@/modules/core/helpers/mainConstants')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { getFreeServerPort } = require('@/test/serverHelper')
|
||||
|
||||
let addr
|
||||
@@ -100,14 +100,14 @@ describe('GraphQL API Subscriptions @gql-subscriptions', () => {
|
||||
|
||||
userA.id = await createUser(userA)
|
||||
const token = await createPersonalAccessToken(userA.id, 'test token user A', [
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
])
|
||||
userA.token = `Bearer ${token}`
|
||||
|
||||
@@ -116,14 +116,14 @@ describe('GraphQL API Subscriptions @gql-subscriptions', () => {
|
||||
userB.id,
|
||||
'test token user B',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('GraphQL API Subscriptions @gql-subscriptions', () => {
|
||||
userC.token = `Bearer ${await createPersonalAccessToken(
|
||||
userC.id,
|
||||
'test token user B',
|
||||
['streams:read', 'streams:write', 'users:read', 'users:email']
|
||||
[Scopes.Streams.Read, Scopes.Streams.Write, Scopes.Users.Read, Scopes.Users.Email]
|
||||
)}`
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const { createManyObjects } = require('@/test/helpers')
|
||||
const { createUser } = require('../services/users')
|
||||
const { createPersonalAccessToken } = require('../services/tokens')
|
||||
const { createStream } = require('../services/streams')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
describe('Upload/Download Routes @api-rest', () => {
|
||||
const userA = {
|
||||
@@ -40,14 +41,14 @@ describe('Upload/Download Routes @api-rest', () => {
|
||||
userA.id,
|
||||
'test token user A',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
@@ -56,14 +57,14 @@ describe('Upload/Download Routes @api-rest', () => {
|
||||
userB.id,
|
||||
'test token user B',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
updateStream,
|
||||
deleteStream,
|
||||
getStreamUsers,
|
||||
grantPermissionsStream,
|
||||
revokePermissionsStream
|
||||
grantPermissionsStream
|
||||
} from '../services/streams'
|
||||
|
||||
import {
|
||||
createBranch,
|
||||
getBranchByNameAndStreamId,
|
||||
@@ -37,7 +37,10 @@ import {
|
||||
createTestStream,
|
||||
createTestStreams
|
||||
} from '@/test/speckle-helpers/streamHelper'
|
||||
import { StreamWithOptionalRole } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
StreamWithOptionalRole,
|
||||
revokeStreamPermissions
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { has, times } from 'lodash'
|
||||
import { Streams } from '@/modules/core/dbSchema'
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
@@ -49,6 +52,7 @@ import {
|
||||
GetUserStreamsQuery
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
import { Get } from 'type-fest'
|
||||
import { changeUserRole } from '@/modules/core/services/users'
|
||||
|
||||
describe('Streams @core-streams', () => {
|
||||
const userOne: BasicTestUser = {
|
||||
@@ -174,7 +178,7 @@ describe('Streams @core-streams', () => {
|
||||
})
|
||||
|
||||
it('Should revoke permissions on stream', async () => {
|
||||
await revokePermissionsStream({ streamId: testStream.id, userId: userTwo.id })
|
||||
await revokeStreamPermissions({ streamId: testStream.id, userId: userTwo.id })
|
||||
const streamWithRole = await getStream({
|
||||
streamId: testStream.id,
|
||||
userId: userTwo.id
|
||||
@@ -183,7 +187,7 @@ describe('Streams @core-streams', () => {
|
||||
})
|
||||
|
||||
it('Should not revoke owner permissions', async () => {
|
||||
await revokePermissionsStream({ streamId: testStream.id, userId: userOne.id })
|
||||
await revokeStreamPermissions({ streamId: testStream.id, userId: userOne.id })
|
||||
.then(() => {
|
||||
throw new Error('This should have thrown')
|
||||
})
|
||||
@@ -215,6 +219,35 @@ describe('Streams @core-streams', () => {
|
||||
const userIsCollaborator = await isStreamCollaborator(userTwo.id, streamId)
|
||||
expect(userIsCollaborator).to.not.be.ok
|
||||
})
|
||||
it('Server guests cannot be stream owners', async () => {
|
||||
const guestGuy: BasicTestUser = {
|
||||
name: 'Some we do not fully trust',
|
||||
email: 'shady@contractor.company',
|
||||
password: 'foobar123',
|
||||
id: ''
|
||||
}
|
||||
|
||||
await createTestUsers([guestGuy])
|
||||
|
||||
await changeUserRole({
|
||||
userId: guestGuy.id,
|
||||
role: Roles.Server.Guest,
|
||||
guestModeEnabled: true
|
||||
})
|
||||
|
||||
await addOrUpdateStreamCollaborator(
|
||||
testStream.id,
|
||||
guestGuy.id,
|
||||
Roles.Stream.Owner,
|
||||
userOne.id
|
||||
)
|
||||
.then(() => {
|
||||
throw new Error('This should have thrown')
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.message).to.include('Server guests cannot own streams')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('`UpdatedAt` prop update', () => {
|
||||
@@ -253,7 +286,7 @@ describe('Streams @core-streams', () => {
|
||||
await grantPermissionsStream({
|
||||
streamId: updatableStream.id,
|
||||
userId: userTwo.id,
|
||||
role: 'stream:contributor'
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
// await sleep(100)
|
||||
@@ -262,7 +295,7 @@ describe('Streams @core-streams', () => {
|
||||
expect(su!.updatedAt).to.not.equal(lastUpdatedAt)
|
||||
lastUpdatedAt = su!.updatedAt
|
||||
|
||||
await revokePermissionsStream({
|
||||
await revokeStreamPermissions({
|
||||
streamId: updatableStream.id,
|
||||
userId: userTwo.id
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ const assert = require('assert')
|
||||
const knex = require('@/db/knex')
|
||||
|
||||
const {
|
||||
archiveUser,
|
||||
changeUserRole,
|
||||
createUser,
|
||||
findOrCreateUser,
|
||||
getUser,
|
||||
@@ -41,6 +41,7 @@ const {
|
||||
|
||||
const { createObject } = require('../services/objects')
|
||||
const { beforeEachContext } = require('@/test/hooks')
|
||||
const { Scopes, Roles } = require('@speckle/shared')
|
||||
|
||||
describe('Actors & Tokens @user-services', () => {
|
||||
const myTestActor = {
|
||||
@@ -193,7 +194,7 @@ describe('Actors & Tokens @user-services', () => {
|
||||
await grantPermissionsStream({
|
||||
streamId: multiOwnerStream.id,
|
||||
userId: myTestActor.id,
|
||||
role: 'stream:owner'
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
|
||||
// create a branch for ballmer on the multiowner stream
|
||||
@@ -292,7 +293,7 @@ describe('Actors & Tokens @user-services', () => {
|
||||
password: 'nanananananaaaa'
|
||||
})
|
||||
|
||||
await archiveUser({ userId: toBeArchivedId })
|
||||
await changeUserRole({ userId: toBeArchivedId, role: Roles.Server.ArchivedUser })
|
||||
|
||||
let { users } = await searchUsers('Library', 20, null)
|
||||
expect(users).to.have.lengthOf(1)
|
||||
@@ -368,24 +369,24 @@ describe('Actors & Tokens @user-services', () => {
|
||||
|
||||
before(async () => {
|
||||
pregeneratedToken = await createPersonalAccessToken(myTestActor.id, 'Whabadub', [
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'profile:read',
|
||||
'users:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Users.Email
|
||||
])
|
||||
revokedToken = await createPersonalAccessToken(myTestActor.id, 'Mr. Revoked', [
|
||||
'streams:read'
|
||||
Scopes.Streams.Read
|
||||
])
|
||||
expireSoonToken = await createPersonalAccessToken(
|
||||
myTestActor.id,
|
||||
'Mayfly',
|
||||
['streams:read'],
|
||||
[Scopes.Streams.Read],
|
||||
1
|
||||
) // 1ms lifespan
|
||||
})
|
||||
|
||||
it('Should create a personal api token', async () => {
|
||||
const scopes = ['streams:write', 'profile:read']
|
||||
const scopes = [Scopes.Streams.Write, Scopes.Profile.Read]
|
||||
const name = 'My Test Token'
|
||||
|
||||
myFirstToken = await createPersonalAccessToken(myTestActor.id, name, scopes)
|
||||
|
||||
@@ -6,11 +6,12 @@ const {
|
||||
getUsers,
|
||||
countUsers,
|
||||
deleteUser,
|
||||
getUserRole,
|
||||
unmakeUserAdmin,
|
||||
makeUserAdmin
|
||||
} = require('../services/users')
|
||||
changeUserRole,
|
||||
getUserRole
|
||||
} = require('@/modules/core/services/users')
|
||||
const { beforeEachContext } = require('@/test/hooks')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
const cryptoRandomString = require('crypto-random-string')
|
||||
|
||||
describe('User admin @user-services', () => {
|
||||
const myTestActor = {
|
||||
@@ -33,7 +34,7 @@ describe('User admin @user-services', () => {
|
||||
const firstUser = users[0]
|
||||
|
||||
const userRole = await getUserRole(firstUser.id)
|
||||
expect(userRole).to.equal('server:admin')
|
||||
expect(userRole).to.equal(Roles.Server.Admin)
|
||||
})
|
||||
|
||||
it('Count user knows how to count', async () => {
|
||||
@@ -52,14 +53,6 @@ describe('User admin @user-services', () => {
|
||||
})
|
||||
|
||||
it('Get users query limit is sanitized to upper limit', async () => {
|
||||
const createNewDroid = (number) => {
|
||||
return {
|
||||
name: `${number}`,
|
||||
email: `${number}@droidarmy.com`,
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
}
|
||||
|
||||
const userInputs = Array(250)
|
||||
.fill()
|
||||
.map((v, i) => createNewDroid(i))
|
||||
@@ -89,27 +82,66 @@ describe('User admin @user-services', () => {
|
||||
expect(await countUsers('droid')).to.equal(250)
|
||||
})
|
||||
|
||||
it('Change user role modifies role', async () => {
|
||||
const [user] = await getUsers(1, 10)
|
||||
describe('changeUserRole', () => {
|
||||
it('throws for invalid role value', async () => {
|
||||
const role = 'shadow:lurker'
|
||||
try {
|
||||
await changeUserRole({ userId: myTestActor.id, role })
|
||||
assert.fail('This should have failed')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal(`Invalid role specified: ${role}`)
|
||||
}
|
||||
})
|
||||
it('throws if guest role not enabled, but trying to change user role to guest', async () => {
|
||||
const role = Roles.Server.Guest
|
||||
try {
|
||||
await changeUserRole({ userId: myTestActor.id, role })
|
||||
assert.fail('This should have failed')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('Guest role is not enabled')
|
||||
}
|
||||
})
|
||||
it('modifies role', async () => {
|
||||
const userId = await createUser(
|
||||
createNewDroid(cryptoRandomString({ length: 13 }))
|
||||
)
|
||||
|
||||
const oldRole = await getUserRole(user.id)
|
||||
expect(oldRole).to.equal('server:user')
|
||||
const oldRole = await getUserRole(userId)
|
||||
expect(oldRole).to.equal(Roles.Server.User)
|
||||
|
||||
await makeUserAdmin({ userId: user.id })
|
||||
let newRole = await getUserRole(user.id)
|
||||
expect(newRole).to.equal('server:admin')
|
||||
await changeUserRole({ userId, role: Roles.Server.Admin })
|
||||
let newRole = await getUserRole(userId)
|
||||
expect(newRole).to.equal(Roles.Server.Admin)
|
||||
|
||||
await unmakeUserAdmin({ userId: user.id })
|
||||
newRole = await getUserRole(user.id)
|
||||
expect(newRole).to.equal('server:user')
|
||||
})
|
||||
await changeUserRole({ userId, role: Roles.Server.User })
|
||||
newRole = await getUserRole(userId)
|
||||
expect(newRole).to.equal(Roles.Server.User)
|
||||
|
||||
it('Ensure at least one admin remains in the server', async () => {
|
||||
try {
|
||||
await unmakeUserAdmin({ userId: myTestActor.id, role: 'server:admin' })
|
||||
assert.fail('This should have failed')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('Cannot remove the last admin role from the server')
|
||||
}
|
||||
await changeUserRole({
|
||||
userId,
|
||||
role: Roles.Server.Guest,
|
||||
guestModeEnabled: true
|
||||
})
|
||||
newRole = await getUserRole(userId)
|
||||
expect(newRole).to.equal(Roles.Server.Guest)
|
||||
})
|
||||
it('Ensures at least one admin remains in the server', async () => {
|
||||
try {
|
||||
await changeUserRole({ userId: myTestActor.id, role: Roles.Server.User })
|
||||
assert.fail('This should have failed')
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal(
|
||||
'Cannot remove the last admin role from the server'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const createNewDroid = (number) => {
|
||||
return {
|
||||
name: `${number}`,
|
||||
email: `${number}@droidarmy.com`,
|
||||
password: 'sn3aky-1337-b1m'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ describe('Activity digest notifications @notifications', () => {
|
||||
canonicalUrl: 'this would be localhost:// or whatever',
|
||||
completed: false,
|
||||
inviteOnly: true,
|
||||
version: 'testing 1 2 3'
|
||||
version: 'testing 1 2 3',
|
||||
guestModeEnabled: false
|
||||
}
|
||||
|
||||
const topic: DigestTopic = {
|
||||
|
||||
@@ -21,6 +21,7 @@ const { moduleLogger, logger } = require('@/logging/logging')
|
||||
const {
|
||||
listenForPreviewGenerationUpdates
|
||||
} = require('@/modules/previews/services/resultListener')
|
||||
const { Scopes, Roles } = require('@speckle/shared')
|
||||
|
||||
const httpErrorImage = (httpErrorCode) =>
|
||||
require.resolve(`#/assets/previews/images/preview_${httpErrorCode}.png`)
|
||||
@@ -144,7 +145,7 @@ exports.init = (app) => {
|
||||
|
||||
if (!stream.isPublic) {
|
||||
try {
|
||||
await validateScopes(req.context.scopes, 'streams:read')
|
||||
await validateScopes(req.context.scopes, Scopes.Streams.Read)
|
||||
} catch (err) {
|
||||
return { hasPermissions: false, httpErrorCode: 401 }
|
||||
}
|
||||
@@ -153,7 +154,7 @@ exports.init = (app) => {
|
||||
await authorizeResolver(
|
||||
req.context.userId,
|
||||
req.params.streamId,
|
||||
'stream:reviewer'
|
||||
Roles.Stream.Reviewer
|
||||
)
|
||||
} catch (err) {
|
||||
return { hasPermissions: false, httpErrorCode: 401 }
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ServerRoles,
|
||||
StreamRoles
|
||||
} from '@/modules/core/helpers/mainConstants'
|
||||
import { getRoles } from '@/modules/shared'
|
||||
import { getRoles } from '@/modules/shared/roles'
|
||||
import { getStream } from '@/modules/core/services/streams'
|
||||
|
||||
import {
|
||||
@@ -114,17 +114,13 @@ export function validateRole<T extends AvailableRoles>({
|
||||
// role validation has nothing to do with auth...
|
||||
//this check doesn't belong here, move it out to the auth pipeline
|
||||
if (!context.auth)
|
||||
return authFailed(
|
||||
context,
|
||||
new UnauthorizedError('Cannot validate role without auth')
|
||||
)
|
||||
return authFailed(context, new UnauthorizedError('Must provide an auth token'))
|
||||
|
||||
const contextRole = roleGetter(context)
|
||||
if (!contextRole)
|
||||
return authFailed(
|
||||
context,
|
||||
new ForbiddenError('You do not have the required role')
|
||||
)
|
||||
const missingRoleMessage = `You do not have the required ${
|
||||
requiredRole.split(':')[0]
|
||||
} role`
|
||||
if (!contextRole) return authFailed(context, new ForbiddenError(missingRoleMessage))
|
||||
|
||||
const role = roles.find((r) => r.name === requiredRole)
|
||||
const myRole = roles.find((r) => r.name === contextRole)
|
||||
@@ -138,7 +134,7 @@ export function validateRole<T extends AvailableRoles>({
|
||||
return authFailed(context, new ForbiddenError('Your role is not valid'))
|
||||
if (myRole.name === iddqd || myRole.weight >= role.weight)
|
||||
return authSuccess(context)
|
||||
return authFailed(context, new ForbiddenError('You do not have the required role'))
|
||||
return authFailed(context, new ForbiddenError(missingRoleMessage))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,3 +273,16 @@ export const streamReadPermissions = [
|
||||
]
|
||||
|
||||
if (adminOverrideEnabled()) streamReadPermissions.push(allowForServerAdmins)
|
||||
|
||||
export const throwForNotHavingServerRole = async (
|
||||
context: AuthContext,
|
||||
requiredRole: ServerRoles
|
||||
) => {
|
||||
const { authResult } = await validateServerRole({ requiredRole })({
|
||||
context,
|
||||
authResult: { authorized: false }
|
||||
})
|
||||
if (authHasFailed(authResult))
|
||||
throw authResult.error ?? new Error('Auth failed without an error')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseError } from '@/modules/shared/errors/base'
|
||||
|
||||
export class ForbiddenError extends BaseError {
|
||||
static code = 'FORBIDDEN_ERROR'
|
||||
static code = 'FORBIDDEN'
|
||||
static defaultMessage = 'Access to the resource is forbidden'
|
||||
}
|
||||
|
||||
|
||||
@@ -11,39 +11,9 @@ const { Roles } = require('@speckle/shared')
|
||||
const { adminOverrideEnabled } = require('@/modules/shared/helpers/envHelper')
|
||||
|
||||
const { ServerAcl: ServerAclSchema } = require('@/modules/core/dbSchema')
|
||||
const { getRoles } = require('@/modules/shared/roles')
|
||||
const ServerAcl = () => ServerAclSchema.knex()
|
||||
|
||||
let roles
|
||||
|
||||
const getRoles = async () => {
|
||||
if (roles) return roles
|
||||
roles = await knex('user_roles').select('*')
|
||||
return roles
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a server role against the req's context object.
|
||||
* @param {import('@/modules/shared/helpers/typeHelper').GraphQLContext} context
|
||||
* @param {string} requiredRole
|
||||
*/
|
||||
async function validateServerRole(context, requiredRole) {
|
||||
const roles = await getRoles()
|
||||
|
||||
if (!context.auth) throw new ForbiddenError('You must provide an auth token.')
|
||||
|
||||
const role = roles.find((r) => r.name === requiredRole)
|
||||
const myRole = roles.find((r) => r.name === context.role)
|
||||
|
||||
if (!role) throw new ApolloError('Invalid server role specified')
|
||||
if (!myRole)
|
||||
throw new ForbiddenError('You do not have the required server role (null)')
|
||||
|
||||
if (context.role === 'server:admin') return true
|
||||
if (myRole.weight >= role.weight) return true
|
||||
|
||||
throw new ForbiddenError('You do not have the required server role')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the scope against a list of scopes of the current session.
|
||||
* @param {string[]|undefined} scopes
|
||||
@@ -65,7 +35,7 @@ async function validateScopes(scopes, scope) {
|
||||
async function authorizeResolver(userId, resourceId, requiredRole) {
|
||||
userId = userId || null
|
||||
|
||||
if (!roles) roles = await knex('user_roles').select('*')
|
||||
const roles = await getRoles()
|
||||
|
||||
// TODO: Cache these results with a TTL of 1 mins or so, it's pointless to query the db every time we get a ping.
|
||||
|
||||
@@ -129,7 +99,7 @@ async function registerOrUpdateRole(role) {
|
||||
module.exports = {
|
||||
registerOrUpdateScope,
|
||||
registerOrUpdateRole,
|
||||
validateServerRole,
|
||||
// validateServerRole,
|
||||
validateScopes,
|
||||
authorizeResolver,
|
||||
pubsub,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const knex = require(`@/db/knex`)
|
||||
let roles
|
||||
|
||||
const getRoles = async () => {
|
||||
if (roles) return roles
|
||||
roles = await knex('user_roles').select('*')
|
||||
return roles
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRoles
|
||||
}
|
||||
@@ -66,7 +66,7 @@ describe('AuthZ @shared', () => {
|
||||
describe('Role validation', () => {
|
||||
const rolesLookup = async () => [
|
||||
{ name: '1', weight: 1 },
|
||||
{ name: '2', weight: 2 },
|
||||
{ name: 'server:2', weight: 2 },
|
||||
{ name: '3', weight: 3 },
|
||||
{ name: 'goku', weight: 9001 },
|
||||
{ name: '42', weight: 42 }
|
||||
@@ -75,15 +75,18 @@ describe('AuthZ @shared', () => {
|
||||
const testData = [
|
||||
{
|
||||
name: 'Having lower privileged role than required results auth failed',
|
||||
requiredRole: '2',
|
||||
requiredRole: 'server:2',
|
||||
context: { auth: true, role: '1' },
|
||||
expectedResult: authFailed(null, new SFE('You do not have the required role'))
|
||||
expectedResult: authFailed(
|
||||
null,
|
||||
new SFE('You do not have the required server role')
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Not having auth fails role validation',
|
||||
requiredRole: '2',
|
||||
requiredRole: 'server:2',
|
||||
context: { auth: false },
|
||||
expectedResult: authFailed(null, new SUE('Cannot validate role without auth'))
|
||||
expectedResult: authFailed(null, new SUE('Must provide an auth token'))
|
||||
},
|
||||
{
|
||||
name: 'Requiring a junk role fails auth',
|
||||
@@ -93,7 +96,7 @@ describe('AuthZ @shared', () => {
|
||||
},
|
||||
{
|
||||
name: 'Having a junk role fails auth',
|
||||
requiredRole: '2',
|
||||
requiredRole: 'server:2',
|
||||
context: { auth: true, role: 'iddqd' },
|
||||
expectedResult: authFailed(null, new SFE('Your role is not valid'))
|
||||
},
|
||||
@@ -101,7 +104,10 @@ describe('AuthZ @shared', () => {
|
||||
name: 'Not having the required level fails',
|
||||
requiredRole: 'goku',
|
||||
context: { auth: true, role: '3' },
|
||||
expectedResult: authFailed(null, new SFE('You do not have the required role'))
|
||||
expectedResult: authFailed(
|
||||
null,
|
||||
new SFE('You do not have the required goku role')
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Having the god mode role defeats even higher privilege requirement',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use strict'
|
||||
const { validateServerRole, validateScopes } = require('@/modules/shared')
|
||||
const { validateScopes } = require('@/modules/shared')
|
||||
const {
|
||||
getStreamHistory,
|
||||
getCommitHistory,
|
||||
@@ -10,6 +10,8 @@ const {
|
||||
getTotalObjectCount,
|
||||
getTotalUserCount
|
||||
} = require('../../services')
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { throwForNotHavingServerRole } = require('@/modules/shared/authz')
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
@@ -17,8 +19,8 @@ module.exports = {
|
||||
* @deprecated('Use admin.serverStatistics')
|
||||
*/
|
||||
async serverStats(parent, args, context) {
|
||||
await validateServerRole(context, 'server:admin')
|
||||
await validateScopes(context.scopes, 'server:stats')
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Admin)
|
||||
await validateScopes(context.scopes, Scopes.Server.Stats)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
getTotalObjectCount,
|
||||
getTotalUserCount
|
||||
} = require('../services')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
const params = { numUsers: 25, numStreams: 30, numObjects: 100, numCommits: 100 }
|
||||
|
||||
@@ -126,24 +127,24 @@ describe('Server stats api @stats-api', function () {
|
||||
adminUser.goodToken = `Bearer ${await createPersonalAccessToken(
|
||||
adminUser.id,
|
||||
'test token user A',
|
||||
['server:stats']
|
||||
[Scopes.Server.Stats]
|
||||
)}`
|
||||
adminUser.badToken = `Bearer ${await createPersonalAccessToken(
|
||||
adminUser.id,
|
||||
'test token user A',
|
||||
['streams:read']
|
||||
[Scopes.Streams.Read]
|
||||
)}`
|
||||
|
||||
notAdminUser.id = await createUser(notAdminUser)
|
||||
notAdminUser.goodToken = `Bearer ${await createPersonalAccessToken(
|
||||
notAdminUser.id,
|
||||
'test token user A',
|
||||
['server:stats']
|
||||
[Scopes.Server.Stats]
|
||||
)}`
|
||||
notAdminUser.badToken = `Bearer ${await createPersonalAccessToken(
|
||||
notAdminUser.id,
|
||||
'test token user A',
|
||||
['streams:read']
|
||||
[Scopes.Streams.Read]
|
||||
)}`
|
||||
|
||||
await seedDb(params)
|
||||
|
||||
@@ -10,11 +10,12 @@ const {
|
||||
getLastWebhookEvents,
|
||||
getWebhookEventsCount
|
||||
} = require('../../services/webhooks')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
module.exports = {
|
||||
Stream: {
|
||||
async webhooks(parent, args, context) {
|
||||
await authorizeResolver(context.userId, parent.id, 'stream:owner')
|
||||
await authorizeResolver(context.userId, parent.id, Roles.Stream.Owner)
|
||||
|
||||
if (args.id) {
|
||||
const wh = await getWebhook({ id: args.id })
|
||||
@@ -41,7 +42,7 @@ module.exports = {
|
||||
|
||||
Mutation: {
|
||||
async webhookCreate(parent, args, context) {
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, 'stream:owner')
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, Roles.Stream.Owner)
|
||||
|
||||
const id = await createWebhook({
|
||||
streamId: args.webhook.streamId,
|
||||
@@ -55,7 +56,7 @@ module.exports = {
|
||||
return id
|
||||
},
|
||||
async webhookUpdate(parent, args, context) {
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, 'stream:owner')
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, Roles.Stream.Owner)
|
||||
|
||||
const wh = await getWebhook({ id: args.webhook.id })
|
||||
if (args.webhook.streamId !== wh.streamId)
|
||||
@@ -75,7 +76,7 @@ module.exports = {
|
||||
return !!updated
|
||||
},
|
||||
async webhookDelete(parent, args, context) {
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, 'stream:owner')
|
||||
await authorizeResolver(context.userId, args.webhook.streamId, Roles.Stream.Owner)
|
||||
|
||||
const wh = await getWebhook({ id: args.webhook.id })
|
||||
if (args.webhook.streamId !== wh.streamId)
|
||||
|
||||
@@ -16,6 +16,7 @@ const {
|
||||
} = require('../services/webhooks')
|
||||
const { createUser } = require('../../core/services/users')
|
||||
const { createStream, grantPermissionsStream } = require('../../core/services/streams')
|
||||
const { Scopes, Roles } = require('@speckle/shared')
|
||||
|
||||
describe('Webhooks @webhooks', () => {
|
||||
let server, sendRequest, app
|
||||
@@ -139,17 +140,17 @@ describe('Webhooks @webhooks', () => {
|
||||
userOne.token = `Bearer ${await createPersonalAccessToken(
|
||||
userOne.id,
|
||||
'userOne test token',
|
||||
['streams:read', 'streams:write']
|
||||
[Scopes.Streams.Read, Scopes.Streams.Write]
|
||||
)}`
|
||||
userTwo.token = `Bearer ${await createPersonalAccessToken(
|
||||
userTwo.id,
|
||||
'userTwo test token',
|
||||
['streams:read', 'streams:write']
|
||||
[Scopes.Streams.Read, Scopes.Streams.Write]
|
||||
)}`
|
||||
await grantPermissionsStream({
|
||||
streamId: streamTwo.id,
|
||||
userId: userOne.id,
|
||||
role: 'stream:contributor'
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const knex = require('@/db/knex')
|
||||
const { logger } = require('@/logging/logging')
|
||||
const roles = require('@/modules/core/roles.js')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
|
||||
const Users = () => knex('users')
|
||||
|
||||
@@ -35,10 +36,10 @@ const migrateColumnValue = async (tableName, columnName, oldUser, newUser) => {
|
||||
const serverAclMigration = async ({ lowerUser, upperUser }) => {
|
||||
const oldAcl = await knex('server_acl').where({ userId: upperUser.id }).first()
|
||||
// if the old user was admin, make the target admin too
|
||||
if (oldAcl.role === 'server:admin')
|
||||
if (oldAcl.role === Roles.Server.Admin)
|
||||
await knex('server_acl')
|
||||
.where({ userId: lowerUser.id })
|
||||
.update({ role: 'server:admin' })
|
||||
.update({ role: Roles.Server.Admin })
|
||||
}
|
||||
|
||||
const _migrateSingleStreamAccess = async ({ lowerUser, upperStreamAcl }) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ const { init } = require(`@/app`)
|
||||
const request = require('supertest')
|
||||
const { exit } = require('yargs')
|
||||
const { logger } = require('@/logging/logging')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
|
||||
const main = async () => {
|
||||
const testStream = {
|
||||
@@ -30,14 +31,14 @@ const main = async () => {
|
||||
userA.id,
|
||||
'test token user A',
|
||||
[
|
||||
'streams:read',
|
||||
'streams:write',
|
||||
'users:read',
|
||||
'users:email',
|
||||
'tokens:write',
|
||||
'tokens:read',
|
||||
'profile:read',
|
||||
'profile:email'
|
||||
Scopes.Streams.Read,
|
||||
Scopes.Streams.Write,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Email,
|
||||
Scopes.Tokens.Write,
|
||||
Scopes.Tokens.Read,
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email
|
||||
]
|
||||
)}`
|
||||
|
||||
|
||||
@@ -1874,6 +1874,7 @@ export type ServerInfo = {
|
||||
canonicalUrl?: Maybe<Scalars['String']>;
|
||||
company?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
guestModeEnabled: Scalars['Boolean'];
|
||||
inviteOnly?: Maybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
roles: Array<Maybe<Role>>;
|
||||
@@ -1886,6 +1887,7 @@ export type ServerInfoUpdateInput = {
|
||||
adminContact?: InputMaybe<Scalars['String']>;
|
||||
company?: InputMaybe<Scalars['String']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
guestModeEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
inviteOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
name: Scalars['String'];
|
||||
termsOfService?: InputMaybe<Scalars['String']>;
|
||||
@@ -1906,6 +1908,7 @@ export type ServerInviteCreateInput = {
|
||||
export enum ServerRole {
|
||||
ServerAdmin = 'SERVER_ADMIN',
|
||||
ServerArchivedUser = 'SERVER_ARCHIVED_USER',
|
||||
ServerGuest = 'SERVER_GUEST',
|
||||
ServerUser = 'SERVER_USER'
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const Roles = Object.freeze(<const>{
|
||||
Server: {
|
||||
Admin: 'server:admin',
|
||||
User: 'server:user',
|
||||
Guest: 'server:guest',
|
||||
ArchivedUser: 'server:archived-user'
|
||||
}
|
||||
})
|
||||
@@ -47,6 +48,10 @@ export const Scopes = Object.freeze(<const>{
|
||||
Tokens: {
|
||||
Read: 'tokens:read',
|
||||
Write: 'tokens:write'
|
||||
},
|
||||
Apps: {
|
||||
Read: 'apps:read',
|
||||
Write: 'apps:write'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user