Files
speckle-server/packages/server/modules/shared/index.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

214 lines
6.1 KiB
JavaScript

'use strict'
const Redis = require('ioredis')
const knex = require(`@/db/knex`)
const { ForbiddenError, ApolloError } = require('apollo-server-express')
const { RedisPubSub } = require('graphql-redis-subscriptions')
const { buildRequestLoaders } = require('@/modules/core/loaders')
const { validateToken } = require(`@/modules/core/services/tokens`)
const pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL)
})
/**
* @typedef {Object} AuthContextPart
* @property {boolean} auth Whether or not user is logged in
* @property {string | undefined} userId User ID, if user is logged in
* @property {string | undefined} role User role, if logged in
* @property {string | undefined} token User token, if logged in
* @property {string[] | undefined} scopes Token scopes, if logged in
*/
/**
* @typedef {AuthContextPart & {loaders: import('@/modules/core/loaders').RequestDataLoaders}} GraphQLContext
*/
/**
* Add data loaders to auth ctx
* @param {AuthContextPart} ctx
* @returns {GraphQLContext}
*/
async function addLoadersToCtx(ctx) {
const loaders = buildRequestLoaders(ctx)
ctx.loaders = loaders
return ctx
}
/**
* Build context for GQL operations
* @returns {GraphQLContext}
*/
async function buildContext({ req, connection }) {
// Parsing auth info
const ctx = await contextApiTokenHelper({ req, connection })
// Adding request data loaders
return addLoadersToCtx(ctx)
}
/**
* Not just Graphql server context helper: sets req.context to have an auth prop (true/false), userId and server role.
* @returns {AuthContextPart}
*/
async function contextApiTokenHelper({ req, connection }) {
let token = null
if (connection && connection.context.token) {
// Websockets (subscriptions)
token = connection.context.token
} else if (req && req.headers.authorization) {
// Standard http post
token = req.headers.authorization
}
if (token && token.includes('Bearer ')) {
token = token.split(' ')[1]
}
if (token === null) return { auth: false }
try {
const { valid, scopes, userId, role } = await validateToken(token)
if (!valid) {
return { auth: false }
}
return { auth: true, userId, role, token, scopes }
} catch (e) {
// TODO: Think whether perhaps it's better to throw the error
return { auth: false, err: e }
}
}
/**
* Express middleware wrapper around the buildContext function. sets req.context to have an auth prop (true/false), userId and server role.
*/
async function contextMiddleware(req, res, next) {
const result = await buildContext({ req, res })
req.context = result
next()
}
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 {[type]} context [description]
* @param {[type]} requiredRole [description]
* @return {[type]} [description]
*/
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 {[type]} scopes [description]
* @param {[type]} scope [description]
* @return {[type]} [description]
*/
async function validateScopes(scopes, scope) {
if (!scopes) throw new ForbiddenError('You do not have the required privileges.')
if (scopes.indexOf(scope) === -1 && scopes.indexOf('*') === -1)
throw new ForbiddenError('You do not have the required privileges.')
}
/**
* Checks the userId against the resource's acl.
* @param {[type]} userId [description]
* @param {[type]} resourceId [description]
* @param {[type]} requiredRole [description]
* @return {[type]} [description]
*/
async function authorizeResolver(userId, resourceId, requiredRole) {
if (!roles) roles = await knex('user_roles').select('*')
// 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.
const role = roles.find((r) => r.name === requiredRole)
if (!role) throw new ApolloError('Unknown role: ' + requiredRole)
try {
const { isPublic } = await knex(role.resourceTarget)
.select('isPublic')
.where({ id: resourceId })
.first()
if (isPublic && roles[requiredRole] < 200) return true
} catch (e) {
throw new ApolloError(
`Resource of type ${role.resourceTarget} with ${resourceId} not found`
)
}
const userAclEntry = await knex(role.aclTableName)
.select('*')
.where({ resourceId, userId })
.first()
if (!userAclEntry)
throw new ForbiddenError('You do not have access to this resource.')
userAclEntry.role = roles.find((r) => r.name === userAclEntry.role)
if (userAclEntry.role.weight >= role.weight) return userAclEntry.role.name
else throw new ForbiddenError('You are not authorized.')
}
const Scopes = () => knex('scopes')
async function registerOrUpdateScope(scope) {
await knex.raw(
`${Scopes()
.insert(scope)
.toString()} on conflict (name) do update set public = ?, description = ? `,
[scope.public, scope.description]
)
return
}
const Roles = () => knex('user_roles')
async function registerOrUpdateRole(role) {
await knex.raw(
`${Roles()
.insert(role)
.toString()} on conflict (name) do update set weight = ?, description = ?, "resourceTarget" = ? `,
[role.weight, role.description, role.resourceTarget]
)
return
}
module.exports = {
registerOrUpdateScope,
registerOrUpdateRole,
buildContext,
addLoadersToCtx,
contextMiddleware,
validateServerRole,
validateScopes,
authorizeResolver,
pubsub,
getRoles
}