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