UNPKG

@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
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