@flowfuse/flowfuse
Version:
An open source low-code development platform
129 lines (124 loc) • 6.66 kB
JavaScript
const fp = require('fastify-plugin')
const { Permissions } = require('../../lib/permissions')
const { Roles } = require('../../lib/roles.js')
// For device/project tokens, list the scopes they implicitly have.
// This will allow us to add scopes to existing tokens without having to update
// them (as that requires reprovisioning of devices and restaging of projects)
const IMPLICIT_TOKEN_SCOPES = {
device: [
'team:projects:list', // permit a device being edited via a tunnel in developer mode to list projects
'library:entry:create', // permit a device being edited via a tunnel in developer mode to create library entries
'library:entry:list', // permit a device being edited via a tunnel in developer mode to list library entries
'broker:clients:list', // permit ff-mqtt nodes to list broker clients
'broker:clients:link', // permit ff-mqtt nodes to link broker clients
'assistant:call' // permit access to assistant
],
project: [
'user:read',
'project:flows:view',
'project:flows:edit',
'team:projects:list',
'library:entry:create',
'library:entry:list',
'broker:clients:list',
'broker:clients:link',
'assistant:call' // permit access to assistant
]
}
module.exports = fp(async function (app, opts) {
function hasPermission (teamMembership, scope, context) {
if (!teamMembership) {
return false
}
let userRole = teamMembership.role
// Granular RBAC; if the request provides an application context for the request, check against
// the teamMembership.permissions object
if (app.config.features.enabled('rbacApplication')) {
const application = context?.application?.hashid || context?.applicationId
if (application && teamMembership.permissions?.applications?.[application] !== undefined) {
userRole = teamMembership.permissions.applications[application]
}
}
const permission = Permissions[scope]
return userRole >= permission.role
}
function needsPermission (scope) {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
return async (request, reply) => {
if (!request.session.scope && request.session.User && request.session.User.admin) {
// Admins get to have all the fun - as long as they are logged in and not
// using an access-token which has a reduced scope
return
}
// A user has permission based on:
// - the resource they are accessing
// - the action they want to perform
// For all Team based permissions, the request should already have
// request.team and request.teamMembership set
const permission = Permissions[scope]
if (permission.role === Roles.Admin && !request.session.User.admin && !permission.self) {
// Requires admin user
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
} else if (app.settings.get(scope) === false) {
// Permission disabled via admin settings
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
} else if (permission.role && permission.role !== Roles.Admin && (!request.session.scope || request.session.ownerType === 'user')) {
// The user is required to have a role in the team associated with
// this request
if (!request.teamMembership) {
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
}
if (permission.self && (request.user && (request.user.id === request.session.User?.id))) {
// This permission is permitted if the user is operating on themselves
return
}
let userRole = request.teamMembership.role
if (app.config.features.enabled('rbacApplication')) {
// Granular RBAC; if the request provides an application context for the request, check against
// the teamMembership.permissions object
const application = request.application?.hashid || request.applicationId
if (application && request.teamMembership.permissions?.applications?.[application] !== undefined) {
userRole = request.teamMembership.permissions.applications[application]
}
}
// console.log(request.url, scope, request.teamMembership.role, userRole)
if (userRole < permission.role) {
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
}
} else if (permission.self) {
// A request outside the context of a team.
if (!request.session.User) {
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
}
if (request.user) {
// This request is in the context of a user. Ensure it is the
// session user
if (request.user.id !== request.session.User?.id) {
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
}
}
}
if (request.session.scope) {
// All things being equal, the user does have this permission
// But they are using an access_token that could be scoped down
// We also need to check against the list of implicit scopes for
// a given token type (ie device/project)
if (!request.session.scope.includes(scope) &&
(!IMPLICIT_TOKEN_SCOPES[request.session.ownerType] || !IMPLICIT_TOKEN_SCOPES[request.session.ownerType].includes(scope))) {
reply.code(403).send({ code: 'unauthorized', error: 'unauthorized' })
throw new Error()
}
}
}
}
app.decorate('hasPermission', hasPermission)
app.decorate('needsPermission', needsPermission)
}, { name: 'app.routes.auth.permissions' })