@getanthill/datastore
Version:
Event-Sourced Datastore
140 lines (121 loc) • 3.74 kB
text/typescript
import type { NextFunction, Request, Response } from 'express';
import { Access, Services, authorizations as authz } from '../../typings';
import os from 'node:os';
import { getAuthorizationToken } from './authenticate';
export function authorize(services: Services) {
return async (req: Request, res: Response, next: NextFunction) => {
const now = new Date();
const method = req.method.toLowerCase();
const path = `/api${req.path}`;
const fragments: Array<string> = path.split('/').slice(1);
const query = req.query;
const headers = req.headers;
const cookies = req.cookies ?? {};
const token = getAuthorizationToken(req);
const access = services.config.security.tokens.find(
({ token: t }) => t === token,
) as Access | undefined;
const modelConfig = services.models.MODELS.get(fragments[1]);
const model = ['admin', 'stream', 'aggregate', 'graphql'].includes(
fragments[1],
)
? 'na'
: modelConfig?.getModelConfig().name ?? 'unknown';
const correlationId = model === 'na' ? 'na' : fragments[2] ?? 'unknown';
const correlationField = modelConfig?.getCorrelationField() ?? 'unknown';
if (
services.config.authz.onlyModels.length > 0 &&
!services.config.authz.onlyModels.includes(model)
) {
return next();
}
if (
services.config.authz.skipModels.length > 0 &&
services.config.authz.skipModels.includes(model)
) {
return next();
}
const authorizationRequest: authz.AuthorizationRequest = {
action: {
scope: [
'action',
...fragments,
method,
correlationField,
correlationId,
],
method,
path,
query,
headers,
is_stream: model === 'stream',
is_admin: model === 'admin',
is_aggregate: model === 'aggregate',
is_graphql: model === 'graphql',
},
subject: {
...access,
scope: ['subject', access?.id ?? 'na'],
cookies,
token,
access,
},
object: {
scope: ['object'],
model,
correlation_id: correlationId,
correlation_field: correlationField,
},
context: {
scope: ['context'],
date: now.toISOString(),
dow: now.getDay(),
dom: now.getDate(),
year: now.getFullYear(),
month: now.getMonth() + 1,
hostname: os.hostname(),
},
};
const decision = await services.authz.authorize(authorizationRequest);
services.telemetry.logger.debug(
'[middlewares#authorization] Authorization request',
{ request: authorizationRequest, decision },
);
if (decision.verb === authz.AUTHORIZATION_VERB_DENY) {
return next({
status: 403,
message: 'Unauthorized',
});
}
res.locals.authz = { decision, request: authorizationRequest };
return next();
};
}
export function obligations(services: Services) {
return async (req: Request, res: Response, next: NextFunction) => {
const obligations = (res.locals?.authz?.decision.obligations ??
[]) as Array<authz.Obligation>;
if (obligations.length === 0) {
return next();
}
const obj = services.authz.applyObligations(
{
// @ts-ignore
body: res.body,
payload: req.body,
query: req.query,
headers: req.headers,
request: res.locals?.authz?.request,
decision: res.locals?.authz?.decision,
},
obligations,
);
req.body = obj.payload;
req.query = obj.query;
// @ts-ignore
res.body = obj.body;
// @ts-ignore
res.headers = obj.headers;
return next();
};
}