Files
speckle-server/packages/server/modules/shared/authz.js
T
Gergő Jedlicska ed458fb619 Add blob storage backend (#802)
* feat(server): add server authz pipeline rework first sketch

* feat(server authz): add new server authz middleware poc implementation

* test(server authz): add unittests for the new server authz workflow

* feat(wip rework of fileuploads vs blob storage): add basim impl of separate blob storage service

* feat(fileimport service): refactored file import service to utilize the new asssetstorage service

* refactor(server errors): refactor server errors to use the shared module definitions

Now all the errors inherit from BaseError

* refactor(fileimport service): cleanup after refactor

* feat(frontend fileimports): use the new blob storage for downloading the original file

* refactor(server fileimports): clean up the remnants of S3 storage from file imports

* refactor(server authz): centralize generic authz pipeline configs

* refactor(server blob storage): refactor / rename everything to use the `blob-storage` name

* ci(circleci): add s3 objectstorage environment variables

* ci(circleci): fix missing env variables

* ci(circleci): add minio test container

* ci(circleci): fix minio app startup

* ci(circleci): enable circleci remote docker

* ci(circleci): fix minio startup

* ci(cirleci): detach and wait properly for minio to start

* ci(circleci): revert to additional minio img config, it only fails when the container is stopped ?!

* ci(circleci): disable file uploads

* fix(fileimports): update with blob storage refactor leftovers

* feat(server blob storage): add blob storage graphql api

* refactor(server errors): merge new errors to shared module

* fix(server comments rte): fix import for RTE error

* chore(fileimports): remove node-fetch from dependency

* chore(server): remove body parser dependency

* fix(server blob storage): fix gql api

* fix(frontend): fix fileupload item not loading the new upload status, cause of premature event fire

* feat(server blob storage): fix file size limit and allow for public streams

* Update packages/server/modules/blobstorage/graph/schemas/blobstorage.graphql

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>

* chore(blobstorage): fix PR review issues

* fix(server): fix import bugs

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
2022-06-16 11:31:03 +02:00

197 lines
6.5 KiB
JavaScript

const { Roles, Scopes } = require('@/modules/core/helpers/mainConstants')
const { getStream } = require('@/modules/core/services/streams')
const { getRoles } = require('@/modules/shared')
const {
ForbiddenError: SFE,
UnauthorizedError: SUE,
ContextError
} = require('@/modules/shared/errors')
const authFailed = (context, error = null) => ({
context,
authResult: { authorized: false, error }
})
const authSuccess = (context) => ({
context,
authResult: { authorized: true, error: null }
})
const validateRole =
({ requiredRole, rolesLookup, iddqd, roleGetter }) =>
async ({ context, authResult }) => {
const roles = await rolesLookup()
// having the required role doesn't rescue from authResult failure
if (authResult.error) return { context, authResult }
// 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 SUE('Cannot validate role without auth'))
const role = roles.find((r) => r.name === requiredRole)
const myRole = roles.find((r) => r.name === roleGetter(context))
if (!role) return authFailed(context, new SFE('Invalid role requirement specified'))
if (!myRole) return authFailed(context, new SFE('Your role is not valid'))
if (myRole.name === iddqd || myRole.weight >= role.weight)
return authSuccess(context)
return authFailed(context, new SFE('You do not have the required role'))
}
const validateServerRole = ({ requiredRole }) =>
validateRole({
requiredRole,
rolesLookup: getRoles,
iddqd: Roles.Server.Admin,
roleGetter: (context) => context.role
})
const validateStreamRole = ({ requiredRole }) =>
validateRole({
requiredRole,
rolesLookup: getRoles,
iddqd: Roles.Stream.Owner,
roleGetter: (context) => context.stream.role
})
// this could be still useful, if the operation doesnt require a stream context
// const authorizeResolver = refactor the implementation in ../index.js
const validateScope =
({ requiredScope }) =>
async ({ context, authResult }) => {
// having the required role doesn't rescue from authResult failure
if (authResult.error) return { context, authResult }
if (!context.scopes)
return authFailed(context, new SFE('You do not have the required privileges.'))
if (
context.scopes.indexOf(requiredScope) === -1 &&
context.scopes.indexOf('*') === -1
)
return authFailed(context, new SFE('You do not have the required privileges.'))
return authSuccess(context)
}
// this doesn't do any checks on the scopes, its sole responsibility is to add the
// stream object to the pipeline context
const contextRequiresStream =
(streamGetter) =>
// stream getter is an async func over { streamId, userId } returning a stream object
// IoC baby...
async ({ context, authResult, params }) => {
if (!params?.streamId)
return authFailed(
context,
new ContextError("The context doesn't have a streamId")
)
// because we're assigning to the context, it would raise if it would be null
// its probably?? safer than returning a new context
if (!context)
return authFailed(context, new ContextError('The context is not defined'))
// cause stream getter could throw, its not a safe function if we want to
// keep the pipeline rolling
try {
const stream = await streamGetter({
streamId: params.streamId,
userId: context?.userId
})
context.stream = stream
return { context, authResult }
} catch (err) {
// this prob needs some more detailing to not leak internal errors
return authFailed(context, new ContextError(err.message))
}
}
const allowForRegisteredUsersOnPublicStreamsEvenWithoutRole = async ({
context,
authResult
}) =>
context.auth && context.stream.isPublic
? authSuccess(context)
: { context, authResult }
const allowForAllRegisteredUsersOnPublicStreamsWithPublicComments = async ({
context,
authResult
}) =>
context.auth && context.stream.isPublic && context.stream.allowPublicComments
? authSuccess(context)
: { context, authResult }
const authPipelineCreator = (steps) => {
const pipeline = async ({ context, params }) => {
let authResult = { authorized: false, error: null }
for (const step of steps) {
;({ context, authResult } = await step({ context, authResult, params }))
}
// validate auth result a bit...
if (authResult.authorized && authResult.error)
throw new Error('a big fuckup on our end')
return { context, authResult }
}
return pipeline
}
//we could even add an auth middleware creator
// todo move this to a webserver related module, it has no place here
const authMiddlewareCreator = (steps) => {
const pipeline = authPipelineCreator(steps)
const middleware = async (req, res, next) => {
const { authResult } = await pipeline({ context: req.context, params: req.params })
if (!authResult.authorized) {
let message = 'Unknown AuthZ error'
let status = 500
if (authResult.error) {
message = authResult.error.message
if (authResult.error instanceof SUE) status = 401
if (authResult.error instanceof SFE) status = 403
}
return res.status(status).send(message)
}
next()
}
return middleware
}
// eslint-disable-next-line no-unused-vars
const exampleMiddleware = authMiddlewareCreator([
// at some point add the context preparation here too
validateServerRole({ requiredRole: Roles.Server.User }),
validateScope({ requiredScope: Scopes.Streams.Write }),
contextRequiresStream(getStream),
validateStreamRole({ requiredRole: Roles.Stream.Reviewer }),
allowForRegisteredUsersOnPublicStreamsEvenWithoutRole
])
module.exports = {
authPipelineCreator,
authSuccess,
authFailed,
validateRole,
validateScope,
validateServerRole,
validateStreamRole,
contextRequiresStream,
ContextError,
authMiddlewareCreator,
allowForRegisteredUsersOnPublicStreamsEvenWithoutRole,
allowForAllRegisteredUsersOnPublicStreamsWithPublicComments,
streamWritePermissions: [
validateServerRole({ requiredRole: Roles.Server.User }),
validateScope({ requiredScope: Scopes.Streams.Write }),
contextRequiresStream(getStream),
validateStreamRole({ requiredRole: Roles.Stream.Contributor })
],
streamReadPermissions: [
validateServerRole({ requiredRole: Roles.Server.User }),
validateScope({ requiredScope: Scopes.Streams.Read }),
contextRequiresStream(getStream),
validateStreamRole({ requiredRole: Roles.Stream.Contributor })
]
}