@getanthill/datastore
Version:
Event-Sourced Datastore
289 lines (253 loc) • 7.48 kB
text/typescript
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,
);
}
}