Merge pull request #1728 from specklesystems/gergo/serverGuest/main

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