Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 94x 94x 1x 1x 1x 1x 1x 1x 94x 94x 94x 94x 94x 94x 94x 1x 1x 1x 1x 1x 1x 1x 18x 21x 21x 4x 4x 4x 3x 4x 21x 2x 2x 21x 1x 1x 21x 1x 1x 21x 18x 18x 1x 1x 1x 1x 1x 1x 1x 1x 1x 56x 38x 38x 38x 38x 38x 38x 56x 56x 17x 1x 1x 17x 56x 16x 8x 8x 16x 56x 9x 9x 56x 56x 56x 1x | "use strict";
/**
* Request-scoped principals and the operation authorisation policy.
*
* Authorisation is enforced twice: search results are filtered to what the
* caller may use, and every SDK call made during an execution is checked by
* the host broker before it reaches the upstream API. Hiding a method from
* search is not the control; the broker check is.
*
* @module server/authorization
*/
import crypto from "node:crypto";
import type { AuthorizationRule } from "../config.js";
import type { OperationBinding, Principal } from "../types.js";
/**
* Derive an opaque principal id from identifying values, such as an OAuth
* issuer and subject, so that logs and audit records can name a caller
* without recording who they are.
*
* A keyed HMAC-SHA256 rather than a plain hash: subjects are often guessable
* (an email address, an employee number), and the issuer is public, so a
* plain hash of the two could be reversed by anyone holding the logs and a
* list of likely subjects. Without the key, that list is useless. The same
* key gives the same id, so ids are stable for as long as the key is.
*
* @param {String} kind - principal kind
* @param {Array} parts - identifying values (e.g. issuer and subject)
* @param {Buffer|String} key - secret HMAC key
* @return {String} opaque id
*/
export function opaquePrincipalId(
kind: string,
parts: string[],
key: crypto.BinaryLike,
): string {
const h = crypto
.createHmac("sha256", key)
.update(parts.join("\u0000"))
.digest("hex")
.slice(0, 16);
return `${kind}:${h}`;
}
/** Authorisation decision. */
export interface Decision {
allowed: boolean;
policyId: string;
reason: string;
}
/**
* Convert a glob with `*` wildcards to a regular expression.
*
* @private
*/
function _glob(pattern: string): RegExp {
const escaped = pattern
.split("*")
.map((p) => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
.join(".*");
return new RegExp(`^${escaped}$`);
}
/**
* Whether an operation pattern matches a binding. Patterns are globs over
* binding ids ("PetsApi.list*"), `tag:<name>`, `method:<HTTP>` or
* `operationId:<glob>`.
*
* @param {String} pattern - pattern
* @param {Object} op - operation binding
* @return {Boolean} match
*/
export function matchesOperation(
pattern: string,
op: OperationBinding,
): boolean {
if (pattern.startsWith("tag:")) {
return op.tags.includes(pattern.slice(4));
}
if (pattern.startsWith("method:")) {
return op.httpMethod === pattern.slice(7).toUpperCase();
}
if (pattern.startsWith("operationId:")) {
return (
Boolean(op.operationId) && _glob(pattern.slice(12)).test(op.operationId!)
);
}
return _glob(pattern).test(op.id);
}
/**
* Operation authorisation policy.
*/
export class AuthorizationPolicy {
private readonly rules: AuthorizationRule[];
private readonly defaultAllow: boolean;
readonly implicit: boolean;
/**
* @param {Object} [config] - `authorization` configuration; undefined allows
* every operation for every authenticated principal ("implicit-allow-all")
*/
constructor(config?: {
default: "allow" | "deny";
rules: AuthorizationRule[];
}) {
this.implicit = !config;
this.rules = config?.rules ?? [];
this.defaultAllow = config ? config.default === "allow" : true;
}
/**
* Rules applying to a principal.
*
* @private
*/
private _rulesFor(p: Principal): AuthorizationRule[] {
return this.rules.filter((r) => {
if (
r.principals &&
!r.principals.some(
(g) =>
_glob(g).test(p.id) ||
(p.name !== undefined && _glob(g).test(p.name)),
)
) {
return false;
}
if (r.clientIds && !(p.clientId && r.clientIds.includes(p.clientId))) {
return false;
}
if (r.scopes && !r.scopes.every((s) => p.scopes.includes(s))) {
return false;
}
return true;
});
}
/**
* Decide whether a principal may invoke an operation.
*
* @param {Object} p - principal
* @param {Object} op - operation binding
* @return {Object} decision
*/
decide(p: Principal, op: OperationBinding): Decision {
if (this.implicit) {
return {
allowed: true,
policyId: "implicit-allow-all",
reason: "no_policy_configured",
};
}
const rules = this._rulesFor(p);
for (const r of rules) {
if (r.deny.some((pat) => matchesOperation(pat, op))) {
return { allowed: false, policyId: r.id, reason: "rule_deny" };
}
}
for (const r of rules) {
if (r.allow.some((pat) => matchesOperation(pat, op))) {
return { allowed: true, policyId: r.id, reason: "rule_allow" };
}
}
return {
allowed: this.defaultAllow,
policyId: "default",
reason: this.defaultAllow ? "default_allow" : "default_deny",
};
}
}
|