ed458fb619
* 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>
197 lines
6.5 KiB
JavaScript
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 })
|
|
]
|
|
}
|