UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

289 lines (253 loc) 7.48 kB
import { Services, authorizations as authz } from '../typings'; import Ajv, { ValidateFunction } from 'ajv'; import * as jsonpatch from 'fast-json-patch'; import get from 'lodash/get'; import pick from 'lodash/pick'; import { AUTHORIZATION_VERB_ALLOW, AUTHORIZATION_VERB_DENY, Obligation, } from '../typings/authorizations'; export interface AuthzConfig { noPolicyVerb: authz.PolicyVerb; } export default class Authz { private config: AuthzConfig = { noPolicyVerb: authz.AUTHORIZATION_VERB_ALLOW as authz.PolicyVerb, }; private services: Services; private validator: Ajv; constructor(config: AuthzConfig, services: Services) { this.config = { ...this.config, ...config, }; this.services = services; this.validator = new Ajv({ strict: false, useDefaults: false, coerceTypes: true, }); } static getScopes(scope: authz.Scope): Array<authz.Scope> { return scope.reduce( (s: Array<authz.Scope>, c: string, i) => [ ...s, i === 0 ? [c] : [...s[i - 1], c], ], [], ); } applyObligations(obj: any, obligations: Array<Obligation>): any { for (const obligation of obligations) { if (obj[obligation.source] === undefined) { continue; } if (obligation.type === 'patch') { const patch = obligation.value.map((p: { value?: string }) => { if (p.value?.[0] === '{') { p.value = get(obj, p.value.slice(1, -1)); } return p; }); obj[obligation.source] = jsonpatch.applyPatch( obj[obligation.source], patch, ).newDocument; } if (obligation.type === 'pick') { obj[obligation.source] = Array.isArray(obj[obligation.source]) ? obj[obligation.source].map((entity: any) => pick(entity, obligation.value as string[]), ) : pick(obj[obligation.source], obligation.value as string[]); } } return obj; } static areRulesValidated(validations: Array<authz.RuleValidation>): boolean { return validations.reduce( (s, c) => s === true && c.action.is_valid === true && c.subject.is_valid === true && c.object.is_valid === true && c.context.is_valid === true, validations.length > 0, ); } static getDecision( decisions: Array<authz.Decision>, config: AuthzConfig, ): authz.Decision { return decisions.reduce( (s, decision: authz.Decision) => { s.verb = s.verb === AUTHORIZATION_VERB_DENY ? s.verb : decision.verb; s.validations.push(...decision.validations); s.obligations = s.verb === AUTHORIZATION_VERB_DENY ? [] : [...s.obligations, ...decision.obligations]; return s; }, { verb: (decisions.length > 0 ? AUTHORIZATION_VERB_ALLOW : config.noPolicyVerb) as authz.PolicyVerb, obligations: [] as Array<authz.Obligation>, validations: [] as Array<authz.RuleValidation>, }, ); } static isAllowed(decisions: Array<authz.Decision>): boolean { return decisions.reduce((s, decision) => { if (s === false) { return s; } if (Authz.areRulesValidated(decision.validations) === true) { return decision.verb === AUTHORIZATION_VERB_ALLOW; } return decision.verb === AUTHORIZATION_VERB_DENY; }, decisions.length > 0); } static validateScope( validator: Ajv, schema: authz.JSONSchema, obj: object, ): authz.ScopeValidation { const schemaId = schema.$id; let validate: ValidateFunction; if (!schema.$id) { validate = validator.compile(schema); } else { validator.addSchema(schema, schemaId); validate = validator.getSchema(schema.$id)!; } const isValid = validate(obj); const errors = validate.errors ?? []; return { is_valid: isValid, errors, }; } static validateRules( validator: Ajv, request: authz.AuthorizationRequest, attributes: authz.RequestAttributes, rules: Array<authz.Rule>, ): Array<authz.RuleValidation> { return rules.map((rule) => { return { action: Authz.validateScope(validator, rule.action, { ...request.action, _attributes: attributes.action, }), subject: Authz.validateScope(validator, rule.subject, { ...request.subject, _attributes: attributes.subject, }), object: Authz.validateScope(validator, rule.object, { ...request.object, _attributes: attributes.object, }), context: Authz.validateScope(validator, rule.context, { ...request.context, _attributes: attributes.context, }), }; }); } static validatePolicy( validator: Ajv, request: authz.AuthorizationRequest, attributes: authz.RequestAttributes, policy: authz.Policy, ): authz.Decision { const validations = Authz.validateRules( validator, request, attributes, policy.rules, ); if (Authz.areRulesValidated(validations) === false) { return { verb: AUTHORIZATION_VERB_ALLOW as authz.PolicyVerb, validations, obligations: [], }; } return { verb: policy.verb, validations, obligations: policy.obligations, }; } static validatePolicies( validator: Ajv, request: authz.AuthorizationRequest, attributes: authz.RequestAttributes, policies: Array<authz.Policy>, config: AuthzConfig, ): authz.Decision { const decisions = policies.map((policy) => Authz.validatePolicy(validator, request, attributes, policy), ); return Authz.getDecision(decisions, config); } async getScopeAttributes( scope: authz.Scope, ): Promise<Array<authz.Attribute>> { return this.services.models .getModel('attributes') .find(this.services.mongodb, { is_enabled: true, scope: { $in: Authz.getScopes(scope), }, }) .toArray(); } async getRequestAttributes( request: authz.AuthorizationRequest, ): Promise<authz.RequestAttributes> { return { action: await this.getScopeAttributes(request.action.scope), subject: await this.getScopeAttributes(request.subject.scope), object: await this.getScopeAttributes(request.object.scope), context: await this.getScopeAttributes(request.context.scope), }; } async getRequestPolicies( request: authz.AuthorizationRequest, ): Promise<Array<authz.Policy>> { return this.services.models .getModel('policies') .find(this.services.mongodb, { is_enabled: true, scope: { $in: [ ...Authz.getScopes(request.action.scope), ...Authz.getScopes(request.subject.scope), ...Authz.getScopes(request.object.scope), ...Authz.getScopes(request.context.scope), ], }, }) .toArray(); } async authorize( request: authz.AuthorizationRequest, ): Promise<authz.Decision> { const [attributes, policies] = await Promise.all([ this.getRequestAttributes(request), this.getRequestPolicies(request), ]); return Authz.validatePolicies( this.validator, request, attributes, policies, this.config, ); } }