diff --git a/packages/server/modules/core/domain/streams/operations.ts b/packages/server/modules/core/domain/streams/operations.ts index ee744cc14..87488a2d1 100644 --- a/packages/server/modules/core/domain/streams/operations.ts +++ b/packages/server/modules/core/domain/streams/operations.ts @@ -20,6 +20,7 @@ import { import { ContextResourceAccessRules } from '@/modules/core/helpers/token' import { MaybeNullOrUndefined, Nullable, Optional, StreamRoles } from '@speckle/shared' import { Knex } from 'knex' +import type express from 'express' export type LegacyGetStreams = (params: { cursor?: string | Date | null | undefined @@ -361,3 +362,11 @@ export type AdminGetProjectList = (args: { items: Stream[] totalCount: number }> + +export type ValidatePermissionsReadStream = ( + streamId: string, + req: express.Request +) => Promise<{ + result: boolean + status: number +}> diff --git a/packages/server/modules/core/rest/authUtils.js b/packages/server/modules/core/rest/authUtils.js index 18573851e..9e00915bc 100644 --- a/packages/server/modules/core/rest/authUtils.js +++ b/packages/server/modules/core/rest/authUtils.js @@ -4,54 +4,8 @@ const { validateScopes, authorizeResolver } = require('@/modules/shared') const { Roles, Scopes } = require('@speckle/shared') const { throwForNotHavingServerRole } = require('@/modules/shared/authz') const { DatabaseError } = require('@/modules/shared/errors') -const { getStreamFactory } = require('@/modules/core/repositories/streams') -const { db } = require('@/db/knex') module.exports = { - async validatePermissionsReadStream(streamId, req) { - const getStream = getStreamFactory({ db }) - const stream = await getStream({ streamId, userId: req.context.userId }) - if (stream?.isPublic) return { result: true, status: 200 } - - try { - await throwForNotHavingServerRole(req.context, Roles.Server.Guest) - } catch (e) { - if (e instanceof DatabaseError) return { result: false, status: 500 } - req.log.info({ err: e }, 'Error while checking stream contributor role') - return { result: false, status: 401 } - } - - if (!stream) return { result: false, status: 404 } - - if (!stream.isPublic && req.context.auth === false) { - req.log.debug('User is not authenticated, so cannot read from non-public stream.') - return { result: false, status: 401 } - } - - if (!stream.isPublic) { - try { - await validateScopes(req.context.scopes, Scopes.Streams.Read) - } catch (e) { - req.log.info({ err: e }, 'Error while validating scopes') - return { result: false, status: 401 } - } - - try { - await authorizeResolver( - req.context.userId, - streamId, - Roles.Stream.Reviewer, - req.context.resourceAccessRules - ) - } catch (e) { - if (e instanceof DatabaseError) return { result: false, status: 500 } - req.log.info({ err: e }, 'Error while checking stream contributor role') - return { result: false, status: 401 } - } - } - return { result: true, status: 200 } - }, - async validatePermissionsWriteStream(streamId, req) { if (!req.context || !req.context.auth) { req.log.debug('User is not authenticated, so cannot write to stream.') diff --git a/packages/server/modules/core/rest/diffDownload.ts b/packages/server/modules/core/rest/diffDownload.ts index e9d4e1f01..f83375b95 100644 --- a/packages/server/modules/core/rest/diffDownload.ts +++ b/packages/server/modules/core/rest/diffDownload.ts @@ -1,14 +1,21 @@ import zlib from 'zlib' import { corsMiddleware } from '@/modules/core/configs/cors' import type { Application } from 'express' -import { validatePermissionsReadStream } from '@/modules/core/rest/authUtils' import { SpeckleObjectsStream } from '@/modules/core/rest/speckleObjectsStream' import { pipeline, PassThrough } from 'stream' import { getObjectsStreamFactory } from '@/modules/core/repositories/objects' import { db } from '@/db/knex' +import { validatePermissionsReadStreamFactory } from '@/modules/core/services/streams/auth' +import { getStreamFactory } from '@/modules/core/repositories/streams' +import { authorizeResolver, validateScopes } from '@/modules/shared' export default (app: Application) => { const getObjectsStream = getObjectsStreamFactory({ db }) + const validatePermissionsReadStream = validatePermissionsReadStreamFactory({ + getStream: getStreamFactory({ db }), + validateScopes, + authorizeResolver + }) app.options('/api/getobjects/:streamId', corsMiddleware()) diff --git a/packages/server/modules/core/rest/download.js b/packages/server/modules/core/rest/download.js index bb0589091..6a29a0e06 100644 --- a/packages/server/modules/core/rest/download.js +++ b/packages/server/modules/core/rest/download.js @@ -2,8 +2,6 @@ const zlib = require('zlib') const { corsMiddleware } = require('@/modules/core/configs/cors') -const { validatePermissionsReadStream } = require('./authUtils') - const { SpeckleObjectsStream } = require('./speckleObjectsStream') const { pipeline, PassThrough } = require('stream') const { logger } = require('@/logging/logging') @@ -12,9 +10,19 @@ const { getObjectChildrenStreamFactory } = require('@/modules/core/repositories/objects') const { db } = require('@/db/knex') +const { + validatePermissionsReadStreamFactory +} = require('@/modules/core/services/streams/auth') +const { getStreamFactory } = require('@/modules/core/repositories/streams') +const { validateScopes, authorizeResolver } = require('@/modules/shared') const getObject = getFormattedObjectFactory({ db }) const getObjectChildrenStream = getObjectChildrenStreamFactory({ db }) +const validatePermissionsReadStream = validatePermissionsReadStreamFactory({ + getStream: getStreamFactory({ db }), + validateScopes, + authorizeResolver +}) module.exports = (app) => { app.options('/objects/:streamId/:objectId', corsMiddleware()) diff --git a/packages/server/modules/core/scopes.js b/packages/server/modules/core/scopes.ts similarity index 93% rename from packages/server/modules/core/scopes.js rename to packages/server/modules/core/scopes.ts index df421ca20..1339a9a87 100644 --- a/packages/server/modules/core/scopes.js +++ b/packages/server/modules/core/scopes.ts @@ -1,7 +1,6 @@ -'use strict' -const { Scopes } = require('@/modules/core/helpers/mainConstants') +import { Scopes } from '@/modules/core/helpers/mainConstants' -module.exports = [ +export default [ { name: Scopes.Streams.Read, description: diff --git a/packages/server/modules/core/services/streams/auth.ts b/packages/server/modules/core/services/streams/auth.ts new file mode 100644 index 000000000..2dae37da8 --- /dev/null +++ b/packages/server/modules/core/services/streams/auth.ts @@ -0,0 +1,57 @@ +import { + GetStream, + ValidatePermissionsReadStream +} from '@/modules/core/domain/streams/operations' +import { throwForNotHavingServerRole } from '@/modules/shared/authz' +import { AuthorizeResolver, ValidateScopes } from '@/modules/shared/domain/operations' +import { DatabaseError } from '@/modules/shared/errors' +import { Roles, Scopes } from '@speckle/shared' + +export const validatePermissionsReadStreamFactory = + (deps: { + getStream: GetStream + validateScopes: ValidateScopes + authorizeResolver: AuthorizeResolver + }): ValidatePermissionsReadStream => + async (streamId, req) => { + const stream = await deps.getStream({ streamId, userId: req.context.userId }) + if (stream?.isPublic) return { result: true, status: 200 } + + try { + await throwForNotHavingServerRole(req.context, Roles.Server.Guest) + } catch (e) { + if (e instanceof DatabaseError) return { result: false, status: 500 } + req.log.info({ err: e }, 'Error while checking stream contributor role') + return { result: false, status: 401 } + } + + if (!stream) return { result: false, status: 404 } + + if (!stream.isPublic && req.context.auth === false) { + req.log.debug('User is not authenticated, so cannot read from non-public stream.') + return { result: false, status: 401 } + } + + if (!stream.isPublic) { + try { + await deps.validateScopes(req.context.scopes, Scopes.Streams.Read) + } catch (e) { + req.log.info({ err: e }, 'Error while validating scopes') + return { result: false, status: 401 } + } + + try { + await deps.authorizeResolver( + req.context.userId, + streamId, + Roles.Stream.Reviewer, + req.context.resourceAccessRules + ) + } catch (e) { + if (e instanceof DatabaseError) return { result: false, status: 500 } + req.log.info({ err: e }, 'Error while checking stream contributor role') + return { result: false, status: 401 } + } + } + return { result: true, status: 200 } + }