@teikei/api
Version:
Teikei API server. Teikei is the software that powers ernte-teilen.org, a website that maps out Community-supported Agriculture in Germany.
142 lines (115 loc) • 3.82 kB
JavaScript
import decode from 'jwt-decode'
import { Forbidden } from '@feathersjs/errors'
import { Ability, AbilityBuilder } from '@casl/ability'
import _ from 'lodash'
Ability.addAlias('update', 'patch')
Ability.addAlias('read', ['get', 'find'])
Ability.addAlias('delete', 'remove')
// hardcoded roles that must match roles defined in database (for now)
const ROLE_USER = 'user'
const ROLE_ADMIN = 'admin'
const ROLE_SUPERADMIN = 'superadmin'
const subjectName = subject =>
!subject || typeof subject === 'string' ? subject : subject.type()
const extractRolesFromJwtToken = ctx =>
ctx.params.headers && ctx.params.headers.authorization
? decode(ctx.params.headers.authorization).roles
: []
const defineAbilities = ctx => {
const roles = extractRolesFromJwtToken(ctx)
if (!ctx.params.user) {
ctx.params.user = {
id: -1,
name: 'guest'
}
}
const userId = ctx.params.user.id
ctx.app.debug(
roles.length > 0 ? roles.map(r => r.name) : 'none',
`authorized user ${userId} ${ctx.params.user.name} with roles`
)
const hasRole = role => roles.find(r => r.name === role)
const { rules, can } = AbilityBuilder.extract()
// admin backend
if (hasRole(ROLE_SUPERADMIN)) {
// TODO: can manage everything in admin backend
} else if (hasRole(ROLE_ADMIN)) {
// TODO: can manage entities in admin backend, but no user accounts/roles
}
// app
if (hasRole(ROLE_USER)) {
can('create', 'autocomplete')
can('create', 'geocoder')
can('read', 'entries')
can(['read', 'create'], 'farms')
can(['update', 'delete'], 'farms', { ownerships: userId })
can(['read', 'create'], 'depots')
can(['update', 'delete'], 'depots', { ownerships: userId })
can(['read', 'create'], 'initiatives')
can(['update', 'delete'], 'initiatives', { ownerships: userId })
can('read', 'products')
can('read', 'goals')
} else {
// guest
can('create', 'autocomplete')
can('create', 'geocoder')
can('read', 'entries')
can('read', 'farms')
can('read', 'depots')
can('read', 'initiatives')
can('read', 'products')
can('read', 'goals')
}
// login
can('create', 'authentication')
// confirm email
can('create', 'authManagement')
// sign up
can('create', 'users')
// edit user account
can('patch', 'users', { id: userId })
return new Ability(rules, { subjectName })
}
const filterFor = condition => {
switch (condition) {
case 'ownerships':
return (resource, value) =>
resource.ownerships.some(o => o.id === value.toString())
default:
return (resource, value) => resource[condition] === value
}
}
const checkConditions = (id, resource, conditions) =>
_.keys(conditions).every(name => filterFor(name)(resource, conditions[name]))
const authorize = async ctx => {
const { method: action, service, path: serviceName } = ctx
const ability = defineAbilities(ctx)
const throwUnlessCan = (a, resource) => {
if (ability.cannot(a, resource)) {
throw new Forbidden(`You are not allowed to ${a} ${resource}.`)
}
}
throwUnlessCan(action, serviceName)
// collection request (read, create)
if (!ctx.id) {
// TODO also implement condition filter for collections?
return ctx
}
// resource request (update, delete)
const conditions = Object.assign(
{},
...ability.rulesFor(action, serviceName).map(r => r.conditions)
)
const eager = _.keys(conditions).filter(name => name === 'ownerships')
const resource = await service.get(ctx.id, {
query: { $eager: `[${eager.join(',')}]` },
provider: null
})
if (!checkConditions(ctx.id, resource, conditions)) {
throw new Forbidden(
`You are not allowed to ${action} ${resource.type()} ${resource.id}.`
)
}
return ctx
}
export default authorize