204 lines
6.1 KiB
JavaScript
204 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`)
|
|
|
|
let 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)
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
let { 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 }
|
|
}
|
|
|
|
return { auth: false }
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
let result = await buildContext({ req, res })
|
|
req.context = result
|
|
next()
|
|
}
|
|
|
|
let 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) {
|
|
if (!roles) roles = await knex('user_roles').select('*')
|
|
|
|
if (!context.auth) throw new ForbiddenError('You must provide an auth token.')
|
|
if (context.role === 'server:admin') return true
|
|
|
|
let role = roles.find((r) => r.name === requiredRole)
|
|
let myRole = roles.find((r) => r.name === context.role)
|
|
|
|
if (role === null) new ApolloError('Invalid server role specified')
|
|
if (myRole === null) new ForbiddenError('You do not have the required server role (null)')
|
|
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.
|
|
|
|
let role = roles.find((r) => r.name === requiredRole)
|
|
|
|
if (role === undefined || role === null) throw new ApolloError('Unknown role: ' + requiredRole)
|
|
|
|
try {
|
|
let { 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`)
|
|
}
|
|
|
|
let userAclEntry = await knex(role.aclTableName)
|
|
.select('*')
|
|
.where({ resourceId: resourceId, userId: 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
|
|
}
|