@statewalker/webrun-biscuit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/authorizer.d.ts +112 -0
- package/dist/authorizer.d.ts.map +1 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/builder.d.ts +52 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/crypto.d.ts +22 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/datalog.d.ts +203 -0
- package/dist/datalog.d.ts.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3130 -0
- package/dist/parser.d.ts +80 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/print.d.ts +12 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/proto.d.ts +132 -0
- package/dist/proto.d.ts.map +1 -0
- package/dist/version.d.ts +42 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/authorizer.ts +581 -0
- package/src/base64.ts +42 -0
- package/src/builder.ts +360 -0
- package/src/crypto.ts +208 -0
- package/src/datalog.ts +722 -0
- package/src/index.ts +93 -0
- package/src/parser.ts +606 -0
- package/src/print.ts +147 -0
- package/src/proto.ts +770 -0
- package/src/version.ts +158 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a serialized token plus authorizer code into an authorization result,
|
|
3
|
+
* mirroring the evaluation order of the reference implementation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { revocationIds, verifyToken } from "./crypto.js";
|
|
7
|
+
import {
|
|
8
|
+
AUTHORIZER,
|
|
9
|
+
type Check,
|
|
10
|
+
DEFAULT_LIMITS,
|
|
11
|
+
ExecutionError,
|
|
12
|
+
type ExternFn,
|
|
13
|
+
type Fact,
|
|
14
|
+
type MapKey,
|
|
15
|
+
normalizeSet,
|
|
16
|
+
type Op,
|
|
17
|
+
Origin,
|
|
18
|
+
type Predicate,
|
|
19
|
+
type Rule,
|
|
20
|
+
type RunLimits,
|
|
21
|
+
type Scope,
|
|
22
|
+
type Term,
|
|
23
|
+
TrustedOrigins,
|
|
24
|
+
trustedOriginsFromScopes,
|
|
25
|
+
World,
|
|
26
|
+
} from "./datalog.js";
|
|
27
|
+
import { Parser, type Statement } from "./parser.js";
|
|
28
|
+
import { printCheck, printPolicy, printPredicate, printRule } from "./print.js";
|
|
29
|
+
import {
|
|
30
|
+
type BlockMsg,
|
|
31
|
+
decodeBiscuit,
|
|
32
|
+
decodeBlock,
|
|
33
|
+
type MapEntryMsg,
|
|
34
|
+
type OpMsg,
|
|
35
|
+
type PredicateMsg,
|
|
36
|
+
ProtoError,
|
|
37
|
+
type RuleMsg,
|
|
38
|
+
type ScopeMsg,
|
|
39
|
+
type TermMsg,
|
|
40
|
+
} from "./proto.js";
|
|
41
|
+
import { validateBlockVersion } from "./version.js";
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_SYMBOLS = [
|
|
44
|
+
"read",
|
|
45
|
+
"write",
|
|
46
|
+
"resource",
|
|
47
|
+
"operation",
|
|
48
|
+
"right",
|
|
49
|
+
"time",
|
|
50
|
+
"role",
|
|
51
|
+
"owner",
|
|
52
|
+
"tenant",
|
|
53
|
+
"namespace",
|
|
54
|
+
"user",
|
|
55
|
+
"team",
|
|
56
|
+
"service",
|
|
57
|
+
"admin",
|
|
58
|
+
"email",
|
|
59
|
+
"group",
|
|
60
|
+
"member",
|
|
61
|
+
"ip_address",
|
|
62
|
+
"client",
|
|
63
|
+
"client_ip",
|
|
64
|
+
"domain",
|
|
65
|
+
"path",
|
|
66
|
+
"version",
|
|
67
|
+
"cluster",
|
|
68
|
+
"node",
|
|
69
|
+
"hostname",
|
|
70
|
+
"nonce",
|
|
71
|
+
"query",
|
|
72
|
+
];
|
|
73
|
+
const OFFSET = 1024;
|
|
74
|
+
|
|
75
|
+
export class TokenError extends Error {
|
|
76
|
+
constructor(
|
|
77
|
+
readonly kind: "Format" | "Symbol",
|
|
78
|
+
message: string,
|
|
79
|
+
) {
|
|
80
|
+
super(message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hex = (b: Uint8Array): string =>
|
|
85
|
+
Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
86
|
+
const keyString = (algorithm: number, key: Uint8Array): string =>
|
|
87
|
+
`${algorithm === 0 ? "ed25519" : "secp256r1"}/${hex(key)}`;
|
|
88
|
+
|
|
89
|
+
/* ------------------------------------------------------ symbol resolution */
|
|
90
|
+
|
|
91
|
+
class Symbols {
|
|
92
|
+
constructor(
|
|
93
|
+
private readonly table: string[],
|
|
94
|
+
private readonly keys: string[],
|
|
95
|
+
) {}
|
|
96
|
+
get(index: number): string {
|
|
97
|
+
if (index < OFFSET) {
|
|
98
|
+
const s = DEFAULT_SYMBOLS[index];
|
|
99
|
+
if (s === undefined) throw new TokenError("Symbol", `unknown default symbol ${index}`);
|
|
100
|
+
return s;
|
|
101
|
+
}
|
|
102
|
+
const s = this.table[index - OFFSET];
|
|
103
|
+
if (s === undefined) throw new TokenError("Symbol", `unknown symbol ${index}`);
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
publicKey(index: number): string {
|
|
107
|
+
const k = this.keys[index];
|
|
108
|
+
if (k === undefined) throw new TokenError("Symbol", `unknown public key ${index}`);
|
|
109
|
+
return k;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ------------------------------------------------- proto -> runtime model */
|
|
114
|
+
|
|
115
|
+
function convTerm(t: TermMsg, s: Symbols): Term {
|
|
116
|
+
switch (t.kind) {
|
|
117
|
+
case "variable":
|
|
118
|
+
return { t: "var", v: t.value };
|
|
119
|
+
case "integer":
|
|
120
|
+
return { t: "int", v: t.value };
|
|
121
|
+
case "string":
|
|
122
|
+
return { t: "str", v: s.get(t.value) };
|
|
123
|
+
case "date":
|
|
124
|
+
return { t: "date", v: t.value };
|
|
125
|
+
case "bytes":
|
|
126
|
+
return { t: "bytes", v: t.value };
|
|
127
|
+
case "bool":
|
|
128
|
+
return { t: "bool", v: t.value };
|
|
129
|
+
case "null":
|
|
130
|
+
return { t: "null" };
|
|
131
|
+
case "set":
|
|
132
|
+
return { t: "set", v: normalizeSet(t.value.map((x) => convTerm(x, s))) };
|
|
133
|
+
case "array":
|
|
134
|
+
return { t: "array", v: t.value.map((x) => convTerm(x, s)) };
|
|
135
|
+
case "map":
|
|
136
|
+
return {
|
|
137
|
+
t: "map",
|
|
138
|
+
v: t.value.map((e: MapEntryMsg): [MapKey, Term] => [
|
|
139
|
+
e.key.kind === "integer"
|
|
140
|
+
? { t: "int", v: e.key.value }
|
|
141
|
+
: { t: "str", v: s.get(e.key.value) },
|
|
142
|
+
convTerm(e.value, s),
|
|
143
|
+
]),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const convPredicate = (p: PredicateMsg, s: Symbols): Predicate => ({
|
|
149
|
+
name: s.get(p.name),
|
|
150
|
+
terms: p.terms.map((t) => convTerm(t, s)),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function convOps(ops: OpMsg[], s: Symbols): Op[] {
|
|
154
|
+
return ops.map((op): Op => {
|
|
155
|
+
switch (op.kind) {
|
|
156
|
+
case "value":
|
|
157
|
+
return { kind: "value", value: convTerm(op.value, s) };
|
|
158
|
+
case "unary":
|
|
159
|
+
return {
|
|
160
|
+
kind: "unary",
|
|
161
|
+
op: op.op,
|
|
162
|
+
ffi: op.ffiName !== undefined ? s.get(op.ffiName) : undefined,
|
|
163
|
+
};
|
|
164
|
+
case "binary":
|
|
165
|
+
return {
|
|
166
|
+
kind: "binary",
|
|
167
|
+
op: op.op,
|
|
168
|
+
ffi: op.ffiName !== undefined ? s.get(op.ffiName) : undefined,
|
|
169
|
+
};
|
|
170
|
+
case "closure":
|
|
171
|
+
return { kind: "closure", params: op.params, ops: convOps(op.ops, s) };
|
|
172
|
+
default:
|
|
173
|
+
// unreachable for a well-typed OpMsg; loud rather than `undefined`
|
|
174
|
+
throw new ProtoError(`unknown expression op kind ${(op as { kind: string }).kind}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const convScope = (sc: ScopeMsg, s: Symbols): Scope =>
|
|
180
|
+
sc.kind === "type"
|
|
181
|
+
? sc.value === 0
|
|
182
|
+
? { kind: "authority" }
|
|
183
|
+
: { kind: "previous" }
|
|
184
|
+
: { kind: "publicKey", key: s.publicKey(sc.value) };
|
|
185
|
+
|
|
186
|
+
const convRule = (r: RuleMsg, s: Symbols): Rule => ({
|
|
187
|
+
head: convPredicate(r.head, s),
|
|
188
|
+
body: r.body.map((p) => convPredicate(p, s)),
|
|
189
|
+
expressions: r.expressions.map((e) => convOps(e, s)),
|
|
190
|
+
scopes: r.scope.map((sc) => convScope(sc, s)),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export interface RuntimeBlock {
|
|
194
|
+
facts: Fact[];
|
|
195
|
+
rules: Rule[];
|
|
196
|
+
checks: Check[];
|
|
197
|
+
scopes: Scope[];
|
|
198
|
+
externalKey?: string;
|
|
199
|
+
/** variable id -> name, for printing rules in error messages */
|
|
200
|
+
varNames: Map<number, string>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function convBlock(b: BlockMsg, s: Symbols, externalKey?: string): RuntimeBlock {
|
|
204
|
+
const varNames = new Map<number, string>();
|
|
205
|
+
const collectTerm = (t: TermMsg): void => {
|
|
206
|
+
if (t.kind === "variable") varNames.set(t.value, s.get(t.value));
|
|
207
|
+
else if (t.kind === "set" || t.kind === "array") t.value.forEach(collectTerm);
|
|
208
|
+
else if (t.kind === "map")
|
|
209
|
+
t.value.forEach((e) => {
|
|
210
|
+
collectTerm(e.value);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const collectOps = (ops: OpMsg[]): void => {
|
|
214
|
+
for (const op of ops) {
|
|
215
|
+
if (op.kind === "value") collectTerm(op.value);
|
|
216
|
+
else if (op.kind === "closure") {
|
|
217
|
+
// closure parameters are variables that never appear in a predicate
|
|
218
|
+
for (const param of op.params) varNames.set(param, s.get(param));
|
|
219
|
+
collectOps(op.ops);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const collect = (r: RuleMsg): void => {
|
|
224
|
+
for (const p of [r.head, ...r.body]) p.terms.forEach(collectTerm);
|
|
225
|
+
r.expressions.forEach(collectOps);
|
|
226
|
+
};
|
|
227
|
+
for (const r of b.rules) collect(r);
|
|
228
|
+
for (const c of b.checks) for (const q of c.queries) collect(q);
|
|
229
|
+
return {
|
|
230
|
+
varNames,
|
|
231
|
+
facts: b.facts.map((f) => ({ predicate: convPredicate(f, s) })),
|
|
232
|
+
rules: b.rules.map((r) => convRule(r, s)),
|
|
233
|
+
checks: b.checks.map((c) => ({
|
|
234
|
+
queries: c.queries.map((q) => convRule(q, s)),
|
|
235
|
+
kind: c.kind === 1 ? "all" : c.kind === 2 ? "reject" : "one",
|
|
236
|
+
})),
|
|
237
|
+
scopes: b.scope.map((sc) => convScope(sc, s)),
|
|
238
|
+
externalKey,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ------------------------------------------------------------ token model */
|
|
243
|
+
|
|
244
|
+
export interface LoadedToken {
|
|
245
|
+
blocks: RuntimeBlock[];
|
|
246
|
+
publicKeyToBlockIds: Map<string, number[]>;
|
|
247
|
+
revocationIds: string[];
|
|
248
|
+
/** the issuer's key identifier, when the token carries one — callers use it
|
|
249
|
+
* to pick the right root key during rotation */
|
|
250
|
+
rootKeyId?: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function loadToken(
|
|
254
|
+
bytes: Uint8Array,
|
|
255
|
+
rootPublicKey: Uint8Array,
|
|
256
|
+
/** the root key's algorithm: 0 = Ed25519, 1 = secp256r1 */
|
|
257
|
+
rootAlgorithm: 0 | 1 = 0,
|
|
258
|
+
): LoadedToken {
|
|
259
|
+
const token = decodeBiscuit(bytes);
|
|
260
|
+
verifyToken(token, rootPublicKey, rootAlgorithm);
|
|
261
|
+
|
|
262
|
+
const signed = [token.authority, ...token.blocks];
|
|
263
|
+
const decoded = signed.map((sb) => decodeBlock(sb.block));
|
|
264
|
+
|
|
265
|
+
// token-wide tables, extended block by block (third-party blocks are isolated)
|
|
266
|
+
const globalSymbols: string[] = [];
|
|
267
|
+
const globalKeys: string[] = [];
|
|
268
|
+
const extend = (list: string[], items: string[]): void => {
|
|
269
|
+
for (const i of items) if (!list.includes(i)) list.push(i);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const blocks: RuntimeBlock[] = [];
|
|
273
|
+
const publicKeyToBlockIds = new Map<string, number[]>();
|
|
274
|
+
|
|
275
|
+
for (let id = 0; id < decoded.length; id++) {
|
|
276
|
+
const raw = decoded[id];
|
|
277
|
+
const ext = signed[id].externalSignature;
|
|
278
|
+
const externalKey = ext ? keyString(ext.publicKey.algorithm, ext.publicKey.key) : undefined;
|
|
279
|
+
const blockKeys = raw.publicKeys.map((k) => keyString(k.algorithm, k.key));
|
|
280
|
+
|
|
281
|
+
if (externalKey === undefined) {
|
|
282
|
+
// symbols of a first-party block join the shared table
|
|
283
|
+
for (const sym of raw.symbols) {
|
|
284
|
+
if (globalSymbols.includes(sym)) throw new TokenError("Format", "symbol table overlap");
|
|
285
|
+
globalSymbols.push(sym);
|
|
286
|
+
}
|
|
287
|
+
extend(globalKeys, blockKeys);
|
|
288
|
+
const block = convBlock(raw, new Symbols(globalSymbols, globalKeys), undefined);
|
|
289
|
+
validateBlockVersion(raw.version, raw.checks, false, block);
|
|
290
|
+
blocks.push(block);
|
|
291
|
+
} else {
|
|
292
|
+
// third-party block: its own symbol and public key tables
|
|
293
|
+
const block = convBlock(raw, new Symbols(raw.symbols, blockKeys), externalKey);
|
|
294
|
+
validateBlockVersion(raw.version, raw.checks, true, block);
|
|
295
|
+
blocks.push(block);
|
|
296
|
+
extend(globalKeys, blockKeys);
|
|
297
|
+
const ids = publicKeyToBlockIds.get(externalKey) ?? [];
|
|
298
|
+
ids.push(id);
|
|
299
|
+
publicKeyToBlockIds.set(externalKey, ids);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
blocks,
|
|
305
|
+
publicKeyToBlockIds,
|
|
306
|
+
revocationIds: revocationIds(token).map(hex),
|
|
307
|
+
rootKeyId: token.rootKeyId,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Reads the key identifier from a token **without verifying it**, so a caller
|
|
313
|
+
* can choose which root key to verify against. The token is untrusted at this
|
|
314
|
+
* point: the id is a hint, not a claim.
|
|
315
|
+
*/
|
|
316
|
+
export function peekRootKeyId(bytes: Uint8Array): number | undefined {
|
|
317
|
+
return decodeBiscuit(bytes).rootKeyId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** a block rule whose head uses a variable the body never binds is invalid */
|
|
321
|
+
function headVariablesAreBound(rule: Rule): boolean {
|
|
322
|
+
const bound = new Set<number>();
|
|
323
|
+
for (const p of rule.body) for (const t of p.terms) if (t.t === "var") bound.add(t.v);
|
|
324
|
+
return rule.head.terms.every((t) => t.t !== "var" || bound.has(t.v));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ------------------------------------------------------------- authorizer */
|
|
328
|
+
|
|
329
|
+
export type FailedCheck =
|
|
330
|
+
| { source: "authorizer"; checkId: number }
|
|
331
|
+
| { source: "block"; blockId: number; checkId: number };
|
|
332
|
+
|
|
333
|
+
export type AuthorizationResult =
|
|
334
|
+
| { kind: "ok"; policy: number }
|
|
335
|
+
| { kind: "unauthorized"; policy: { allow: number } | { deny: number }; checks: FailedCheck[] }
|
|
336
|
+
| { kind: "noMatchingPolicy"; checks: FailedCheck[] }
|
|
337
|
+
| { kind: "execution"; error: string }
|
|
338
|
+
| { kind: "invalidBlockRule"; blockId: number; rule: string }
|
|
339
|
+
| { kind: "format"; error: string };
|
|
340
|
+
|
|
341
|
+
/** the shape of the `world` snapshot in the official sample corpus */
|
|
342
|
+
export interface WorldSnapshot {
|
|
343
|
+
facts: { origin: (number | null)[]; facts: string[] }[];
|
|
344
|
+
rules: { origin: number; rules: string[] }[];
|
|
345
|
+
checks: { origin: number; checks: string[] }[];
|
|
346
|
+
policies: string[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface AuthorizeDetails {
|
|
350
|
+
result: AuthorizationResult;
|
|
351
|
+
world: WorldSnapshot;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface AuthorizerCode {
|
|
355
|
+
facts: Fact[];
|
|
356
|
+
rules: Rule[];
|
|
357
|
+
checks: Check[];
|
|
358
|
+
policies: { kind: "allow" | "deny"; queries: Rule[] }[];
|
|
359
|
+
scopes: Scope[];
|
|
360
|
+
varNames: Map<number, string>;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function parseAuthorizer(src: string): AuthorizerCode {
|
|
364
|
+
const out: AuthorizerCode = {
|
|
365
|
+
facts: [],
|
|
366
|
+
rules: [],
|
|
367
|
+
checks: [],
|
|
368
|
+
policies: [],
|
|
369
|
+
scopes: [],
|
|
370
|
+
varNames: new Map(),
|
|
371
|
+
};
|
|
372
|
+
const parser = new Parser(src);
|
|
373
|
+
const statements: Statement[] = parser.parse();
|
|
374
|
+
out.varNames = parser.variableNames();
|
|
375
|
+
for (const st of statements) {
|
|
376
|
+
if (st.k === "fact") out.facts.push(st.fact);
|
|
377
|
+
else if (st.k === "rule") out.rules.push(st.rule);
|
|
378
|
+
else if (st.k === "check") out.checks.push(st.check);
|
|
379
|
+
else if (st.k === "policy") out.policies.push({ kind: st.kind, queries: st.queries });
|
|
380
|
+
else out.scopes.push(...st.scopes);
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface AuthorizeOptions {
|
|
386
|
+
limits?: RunLimits;
|
|
387
|
+
/** extern functions callable as `.extern::name(...)` */
|
|
388
|
+
externs?: Map<string, ExternFn>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function authorize(
|
|
392
|
+
token: LoadedToken,
|
|
393
|
+
authorizerSrc: string,
|
|
394
|
+
options: AuthorizeOptions = {},
|
|
395
|
+
): AuthorizationResult {
|
|
396
|
+
return authorizeDetailed(token, authorizerSrc, options).result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const EMPTY_WORLD: WorldSnapshot = { facts: [], rules: [], checks: [], policies: [] };
|
|
400
|
+
|
|
401
|
+
/** Same as `authorize`, and also returns the post-run world, in the shape the
|
|
402
|
+
* official sample corpus records it. */
|
|
403
|
+
export function authorizeDetailed(
|
|
404
|
+
token: LoadedToken,
|
|
405
|
+
authorizerSrc: string,
|
|
406
|
+
options: AuthorizeOptions = {},
|
|
407
|
+
): AuthorizeDetails {
|
|
408
|
+
const limits = options.limits ?? DEFAULT_LIMITS;
|
|
409
|
+
let code: AuthorizerCode;
|
|
410
|
+
try {
|
|
411
|
+
code = parseAuthorizer(authorizerSrc);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return { result: { kind: "format", error: (e as Error).message }, world: EMPTY_WORLD };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const world = new World();
|
|
417
|
+
if (options.externs) world.externs = options.externs;
|
|
418
|
+
const keys = token.publicKeyToBlockIds;
|
|
419
|
+
const blockTrusted: TrustedOrigins[] = [];
|
|
420
|
+
|
|
421
|
+
for (let id = 0; id < token.blocks.length; id++) {
|
|
422
|
+
const block = token.blocks[id];
|
|
423
|
+
const trusted = trustedOriginsFromScopes(block.scopes, TrustedOrigins.default(), id, keys);
|
|
424
|
+
blockTrusted.push(trusted);
|
|
425
|
+
const origin = Origin.of(id);
|
|
426
|
+
for (const f of block.facts) world.addFact(origin, f);
|
|
427
|
+
for (const r of block.rules) {
|
|
428
|
+
if (!headVariablesAreBound(r))
|
|
429
|
+
return {
|
|
430
|
+
result: {
|
|
431
|
+
kind: "invalidBlockRule",
|
|
432
|
+
blockId: 0, // the reference implementation always reports 0 here
|
|
433
|
+
rule: printRule(r, (v) => block.varNames.get(v) ?? String(v)),
|
|
434
|
+
},
|
|
435
|
+
world: EMPTY_WORLD,
|
|
436
|
+
};
|
|
437
|
+
world.addRule(id, trustedOriginsFromScopes(r.scopes, trusted, id, keys), r);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const authorizerTrusted = trustedOriginsFromScopes(
|
|
442
|
+
code.scopes,
|
|
443
|
+
TrustedOrigins.default(),
|
|
444
|
+
AUTHORIZER,
|
|
445
|
+
keys,
|
|
446
|
+
);
|
|
447
|
+
const authorizerOrigin = Origin.of(AUTHORIZER);
|
|
448
|
+
for (const f of code.facts) world.addFact(authorizerOrigin, f);
|
|
449
|
+
for (const r of code.rules)
|
|
450
|
+
world.addRule(
|
|
451
|
+
AUTHORIZER,
|
|
452
|
+
trustedOriginsFromScopes(r.scopes, authorizerTrusted, AUTHORIZER, keys),
|
|
453
|
+
r,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const name = (id: number): ((v: number) => string) =>
|
|
457
|
+
id === AUTHORIZER
|
|
458
|
+
? (v) => code.varNames.get(v) ?? String(v)
|
|
459
|
+
: (v) => token.blocks[id]?.varNames.get(v) ?? String(v);
|
|
460
|
+
|
|
461
|
+
// in the corpus, a fact's authorizer origin is `null`, but a rule's or
|
|
462
|
+
// check's authorizer origin is the raw u64 block id
|
|
463
|
+
const blockId = (id: number): number | null => (id === AUTHORIZER ? null : id);
|
|
464
|
+
const AUTHORIZER_U64 = Number(18446744073709551615n);
|
|
465
|
+
const ruleOrigin = (id: number): number => (id === AUTHORIZER ? AUTHORIZER_U64 : id);
|
|
466
|
+
|
|
467
|
+
const snapshot = (): WorldSnapshot => {
|
|
468
|
+
const factGroups = [...world.facts.values()]
|
|
469
|
+
.filter((b) => b.items.size > 0)
|
|
470
|
+
.map((b) => ({
|
|
471
|
+
origin: b.origin.ids.map(blockId).sort((x, y) => (x ?? -1) - (y ?? -1)),
|
|
472
|
+
facts: [...b.items.values()].map((f) => printPredicate(f.predicate, () => "?")).sort(),
|
|
473
|
+
}))
|
|
474
|
+
.sort((a, x) => compareOrigins(a.origin, x.origin));
|
|
475
|
+
|
|
476
|
+
const ruleGroups = new Map<number, string[]>();
|
|
477
|
+
for (const { origin, rule } of world.rules) {
|
|
478
|
+
const list = ruleGroups.get(origin) ?? [];
|
|
479
|
+
list.push(printRule(rule, name(origin)));
|
|
480
|
+
ruleGroups.set(origin, list);
|
|
481
|
+
}
|
|
482
|
+
const checkGroups: { origin: number; checks: string[] }[] = [];
|
|
483
|
+
code.checks.forEach((c) => {
|
|
484
|
+
const entry = checkGroups.find((g) => g.origin === AUTHORIZER_U64);
|
|
485
|
+
const text = printCheck(c, name(AUTHORIZER));
|
|
486
|
+
if (entry) entry.checks.push(text);
|
|
487
|
+
else checkGroups.push({ origin: AUTHORIZER_U64, checks: [text] });
|
|
488
|
+
});
|
|
489
|
+
token.blocks.forEach((b, id) => {
|
|
490
|
+
if (b.checks.length === 0) return;
|
|
491
|
+
checkGroups.push({ origin: id, checks: b.checks.map((c) => printCheck(c, name(id))) });
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
facts: factGroups,
|
|
496
|
+
rules: [...ruleGroups.entries()]
|
|
497
|
+
.map(([origin, rules]) => ({ origin: ruleOrigin(origin), rules: rules.sort() }))
|
|
498
|
+
.sort((a, b) => (a.origin ?? -1) - (b.origin ?? -1)),
|
|
499
|
+
checks: checkGroups.sort((a, b) => (a.origin ?? -1) - (b.origin ?? -1)),
|
|
500
|
+
policies: code.policies.map((p) => printPolicy(p.kind, p.queries, name(AUTHORIZER))),
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
world.run(limits);
|
|
506
|
+
} catch (e) {
|
|
507
|
+
if (e instanceof ExecutionError)
|
|
508
|
+
return { result: { kind: "execution", error: e.kind }, world: snapshot() };
|
|
509
|
+
throw e;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const errors: FailedCheck[] = [];
|
|
513
|
+
const runCheck = (check: Check, blockId: number, defaults: TrustedOrigins): boolean => {
|
|
514
|
+
for (const query of check.queries) {
|
|
515
|
+
const trusted = trustedOriginsFromScopes(query.scopes, defaults, blockId, keys);
|
|
516
|
+
const res =
|
|
517
|
+
check.kind === "all"
|
|
518
|
+
? world.queryMatchAll(query, trusted)
|
|
519
|
+
: check.kind === "reject"
|
|
520
|
+
? !world.queryMatch(query, blockId, trusted)
|
|
521
|
+
: world.queryMatch(query, blockId, trusted);
|
|
522
|
+
if (res) return true;
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
// 1. authorizer checks
|
|
529
|
+
code.checks.forEach((check, i) => {
|
|
530
|
+
if (!runCheck(check, AUTHORIZER, authorizerTrusted))
|
|
531
|
+
errors.push({ source: "authorizer", checkId: i });
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// 2. authority block checks
|
|
535
|
+
token.blocks[0]?.checks.forEach((check, j) => {
|
|
536
|
+
if (!runCheck(check, 0, blockTrusted[0]))
|
|
537
|
+
errors.push({ source: "block", blockId: 0, checkId: j });
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// 3. policies (first match wins)
|
|
541
|
+
let policyResult: { allow: number } | { deny: number } | null = null;
|
|
542
|
+
outer: for (const [i, policy] of code.policies.entries()) {
|
|
543
|
+
for (const query of policy.queries) {
|
|
544
|
+
const trusted = trustedOriginsFromScopes(query.scopes, authorizerTrusted, AUTHORIZER, keys);
|
|
545
|
+
if (world.queryMatch(query, AUTHORIZER, trusted)) {
|
|
546
|
+
policyResult = policy.kind === "allow" ? { allow: i } : { deny: i };
|
|
547
|
+
break outer;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 4. checks of the remaining blocks
|
|
553
|
+
for (let id = 1; id < token.blocks.length; id++) {
|
|
554
|
+
token.blocks[id].checks.forEach((check, j) => {
|
|
555
|
+
if (!runCheck(check, id, blockTrusted[id]))
|
|
556
|
+
errors.push({ source: "block", blockId: id, checkId: j });
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const result: AuthorizationResult =
|
|
561
|
+
policyResult === null
|
|
562
|
+
? { kind: "noMatchingPolicy", checks: errors }
|
|
563
|
+
: "allow" in policyResult && errors.length === 0
|
|
564
|
+
? { kind: "ok", policy: policyResult.allow }
|
|
565
|
+
: { kind: "unauthorized", policy: policyResult, checks: errors };
|
|
566
|
+
return { result, world: snapshot() };
|
|
567
|
+
} catch (e) {
|
|
568
|
+
if (e instanceof ExecutionError)
|
|
569
|
+
return { result: { kind: "execution", error: e.kind }, world: snapshot() };
|
|
570
|
+
throw e;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function compareOrigins(a: (number | null)[], b: (number | null)[]): number {
|
|
575
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
576
|
+
const x = a[i] === undefined ? -Infinity : (a[i] ?? -1);
|
|
577
|
+
const y = b[i] === undefined ? -Infinity : (b[i] ?? -1);
|
|
578
|
+
if (x !== y) return x - y;
|
|
579
|
+
}
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
package/src/base64.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** URL-safe base64 without padding, the wire form of a Biscuit token.
|
|
2
|
+
* Implemented directly so the library stays runtime-agnostic (no Buffer,
|
|
3
|
+
* no atob/btoa). */
|
|
4
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
5
|
+
const REVERSE = new Map<string, number>([...ALPHABET].map((c, i) => [c, i]));
|
|
6
|
+
REVERSE.set("+", 62);
|
|
7
|
+
REVERSE.set("/", 63);
|
|
8
|
+
|
|
9
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
10
|
+
let out = "";
|
|
11
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
12
|
+
const b0 = bytes[i];
|
|
13
|
+
const b1 = bytes[i + 1];
|
|
14
|
+
const b2 = bytes[i + 2];
|
|
15
|
+
out += ALPHABET[b0 >> 2];
|
|
16
|
+
out += ALPHABET[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)];
|
|
17
|
+
if (b1 === undefined) break;
|
|
18
|
+
out += ALPHABET[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)];
|
|
19
|
+
if (b2 === undefined) break;
|
|
20
|
+
out += ALPHABET[b2 & 63];
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fromBase64(text: string): Uint8Array {
|
|
26
|
+
const s = text.trim().replace(/=+$/, "");
|
|
27
|
+
const out = new Uint8Array(Math.floor((s.length * 3) / 4));
|
|
28
|
+
let o = 0;
|
|
29
|
+
let acc = 0;
|
|
30
|
+
let bits = 0;
|
|
31
|
+
for (const c of s) {
|
|
32
|
+
const v = REVERSE.get(c);
|
|
33
|
+
if (v === undefined) throw new Error(`invalid base64 character ${JSON.stringify(c)}`);
|
|
34
|
+
acc = (acc << 6) | v;
|
|
35
|
+
bits += 6;
|
|
36
|
+
if (bits >= 8) {
|
|
37
|
+
bits -= 8;
|
|
38
|
+
out[o++] = (acc >> bits) & 0xff;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out.subarray(0, o);
|
|
42
|
+
}
|