@o3co/auth.policy-verifier.cedar-wasm 0.14.0 → 0.16.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/README.md +43 -3
- package/dist/wasmEngine.d.mts +20 -16
- package/dist/wasmEngine.d.mts.map +1 -1
- package/dist/wasmEngine.mjs +50 -25
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @o3co/auth.policy-verifier.cedar-wasm
|
|
2
2
|
|
|
3
|
-
Last updated: 2026-09-
|
|
3
|
+
Last updated: 2026-09-26
|
|
4
4
|
|
|
5
5
|
The in-process Cedar engine for
|
|
6
6
|
[`@o3co/auth.policy-verifier.cedar`](../cedar/README.md): the official
|
|
@@ -37,8 +37,13 @@ deployment imports it.
|
|
|
37
37
|
|
|
38
38
|
**Owns** ([`src/wasmEngine.mts`](src/wasmEngine.mts)):
|
|
39
39
|
|
|
40
|
-
-
|
|
41
|
-
|
|
40
|
+
- Parsing each policy file at boot, so a syntax error names its file;
|
|
41
|
+
naming each policy for its file (cedar's `namePolicies`, #199), so Cedar's
|
|
42
|
+
`diagnostics.reason` — and a decision's `determiningPolicies` — say
|
|
43
|
+
`30-forbid-contractors` or `20-rules#2` rather than a positional `policy7`;
|
|
44
|
+
refusing a set where two policies would share an id, a policy in a file
|
|
45
|
+
named only `.cedar`, or a template, which nothing here would link; and
|
|
46
|
+
compiling the set once into wasm memory under an id minted per load.
|
|
42
47
|
- Evaluating each request against that compiled set, synchronously, and
|
|
43
48
|
rendering the bindings' answer as a `CedarDecision`.
|
|
44
49
|
- Naming the revision of the source it compiled on every answer.
|
|
@@ -77,3 +82,38 @@ a dependency change, not a config change.
|
|
|
77
82
|
`@cedar-policy/cedar-wasm` is pinned exactly: Cedar minor releases can carry
|
|
78
83
|
policy-language changes, so upgrades should be deliberate and re-validated,
|
|
79
84
|
not fall out of a range resolution.
|
|
85
|
+
|
|
86
|
+
The re-validation is a suite (#198):
|
|
87
|
+
[`cedar-cli-equivalence.test.mts`](../../tests/integration/src/cedar-cli-equivalence.test.mts)
|
|
88
|
+
holds this engine, behind `CedarPolicyRuleCollector`, to the official `cedar`
|
|
89
|
+
CLI at the pinned version.
|
|
90
|
+
- **What it compares.** For every request of a fixture corpus, the CLI is
|
|
91
|
+
asked exactly the request the collector built, and both evaluators must agree
|
|
92
|
+
on the decision, on the policies that determined it, and on which policies
|
|
93
|
+
raised errors. The request itself must be the one the case records, so a
|
|
94
|
+
change to how requests are built shows as a diff. The corpus covers the cases
|
|
95
|
+
the entity synthesis decides:
|
|
96
|
+
- group membership, direct and transitive;
|
|
97
|
+
- entity references;
|
|
98
|
+
- a missing attribute;
|
|
99
|
+
- `forbid` over `permit`;
|
|
100
|
+
- the context allowlist;
|
|
101
|
+
- `resource.idWhenAbsent`.
|
|
102
|
+
- **Policy names.** Each fixture policy carries `@id` with the id its file
|
|
103
|
+
gives it. The CLI names a policy by `@id`, and otherwise by its position, so
|
|
104
|
+
this is how it answers in this engine's names.
|
|
105
|
+
- **How the versions stay together.** CI installs `cedar-policy-cli` at the
|
|
106
|
+
version read from this package's `package.json`, so bumping the pin moves the
|
|
107
|
+
CLI with it. A CLI at any other version fails the suite's version check.
|
|
108
|
+
- **Running it locally.** Install the CLI at the pinned version, then build
|
|
109
|
+
and run the suite. Without a CLI this engine's half is skipped, with a
|
|
110
|
+
notice. The suite's other half measures the `http` engine against a real
|
|
111
|
+
cedar-agent (#284, see the cedar package's README).
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
cargo install cedar-policy-cli --locked --version \
|
|
115
|
+
"$(node -p 'require("./packages/cedar-wasm/package.json").dependencies["@cedar-policy/cedar-wasm"]')"
|
|
116
|
+
pnpm run build
|
|
117
|
+
CEDAR_CLI="$(command -v cedar)" pnpm --filter @o3co/auth.policy-verifier.integration-tests \
|
|
118
|
+
exec vitest run cedar-cli-
|
|
119
|
+
```
|
package/dist/wasmEngine.d.mts
CHANGED
|
@@ -10,24 +10,28 @@ export interface CedarWasmEngine extends CedarEngine {
|
|
|
10
10
|
* The in-process Cedar engine: the official `@cedar-policy/cedar-wasm`
|
|
11
11
|
* bindings behind the `CedarEngine` port.
|
|
12
12
|
*
|
|
13
|
-
* `load`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* `load` parses every file on its own — so a syntax error names the file that
|
|
14
|
+
* contains it — splits it into its policies, and names each for its file
|
|
15
|
+
* (`namePolicies`): the file's name for a file that holds one, numbered `#1`,
|
|
16
|
+
* `#2`… for one that holds several. Those are the ids Cedar answers with in
|
|
17
|
+
* `diagnostics.reason` and names in `errors`, and the ids the http engine
|
|
18
|
+
* gives cedar-agent, so a corpus laid out one policy per file reads the same
|
|
19
|
+
* under both. Refused at load: two policies that would share an id (`a.cedar`
|
|
20
|
+
* holding two, beside `a#1.cedar`), a policy in a file named only `.cedar`,
|
|
21
|
+
* and a template. The set is then compiled once, at boot, into wasm memory
|
|
22
|
+
* under a per-load id. `isAuthorized` references that id per request and
|
|
23
|
+
* re-parses nothing; the call is synchronous, deterministic, and a few tens of
|
|
24
|
+
* microseconds, so the policy set answers as a plain `Rule`.
|
|
18
25
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
26
|
+
* The wasm module (about 12 MB on disk) is instantiated when the bindings
|
|
27
|
+
* load, which is why this engine lives in its own package: a deployment that
|
|
28
|
+
* evaluates out of process never carries it.
|
|
22
29
|
*
|
|
23
|
-
* It vouches for the revision it evaluated (`confirmsRevision
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* object the caller still holds; reading `source.revision` when answering would
|
|
29
|
-
* be reading "the current revision", which is a claim about the object and not
|
|
30
|
-
* about the policies that were compiled.
|
|
30
|
+
* It vouches for the revision it evaluated (`confirmsRevision`): the set is
|
|
31
|
+
* compiled here, under an id known to nothing else, so an answer through that
|
|
32
|
+
* id comes from the source `load` was handed and names that source's revision,
|
|
33
|
+
* read once at `load`. Reading `source.revision` when answering would describe
|
|
34
|
+
* the caller's object as it is now, not the policies that were compiled.
|
|
31
35
|
*/
|
|
32
36
|
export declare const cedarWasmEngine: CedarWasmEngine;
|
|
33
37
|
//# sourceMappingURL=wasmEngine.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasmEngine.d.mts","sourceRoot":"","sources":["../src/wasmEngine.mts"],"names":[],"mappings":"AAeA,OAAO,EAEN,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"wasmEngine.d.mts","sourceRoot":"","sources":["../src/wasmEngine.mts"],"names":[],"mappings":"AAeA,OAAO,EAEN,KAAK,WAAW,EAKhB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,MAAM,kCAAkC,CAAC;AAE1C,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,EAAG,MAAe,CAAC;AAEtD,oFAAoF;AACpF,MAAM,WAAW,eAAgB,SAAQ,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,OAAO,sBAAsB,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAAC;CAC/C;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,EAAE,eAuC7B,CAAC"}
|
package/dist/wasmEngine.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* `@cedar-policy/cedar-wasm` bindings, compiling it once at load and answering
|
|
6
6
|
* synchronously. The package index registers it.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { CedarEngineError, } from "@o3co/auth.policy-verifier.cedar";
|
|
8
|
+
import { policySetTextToParts, preparsePolicySet, statefulIsAuthorized, } from "@cedar-policy/cedar-wasm/nodejs";
|
|
9
|
+
import { CedarEngineError, namePolicies, } from "@o3co/auth.policy-verifier.cedar";
|
|
10
10
|
/** The name this engine registers under, and the config value that selects it. */
|
|
11
11
|
export const CEDAR_WASM_ENGINE_NAME = "wasm";
|
|
12
12
|
/** Distinguishes concurrently-loaded policy sets inside the one wasm instance. */
|
|
@@ -15,40 +15,41 @@ let policySetCounter = 0;
|
|
|
15
15
|
* The in-process Cedar engine: the official `@cedar-policy/cedar-wasm`
|
|
16
16
|
* bindings behind the `CedarEngine` port.
|
|
17
17
|
*
|
|
18
|
-
* `load`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* `load` parses every file on its own — so a syntax error names the file that
|
|
19
|
+
* contains it — splits it into its policies, and names each for its file
|
|
20
|
+
* (`namePolicies`): the file's name for a file that holds one, numbered `#1`,
|
|
21
|
+
* `#2`… for one that holds several. Those are the ids Cedar answers with in
|
|
22
|
+
* `diagnostics.reason` and names in `errors`, and the ids the http engine
|
|
23
|
+
* gives cedar-agent, so a corpus laid out one policy per file reads the same
|
|
24
|
+
* under both. Refused at load: two policies that would share an id (`a.cedar`
|
|
25
|
+
* holding two, beside `a#1.cedar`), a policy in a file named only `.cedar`,
|
|
26
|
+
* and a template. The set is then compiled once, at boot, into wasm memory
|
|
27
|
+
* under a per-load id. `isAuthorized` references that id per request and
|
|
28
|
+
* re-parses nothing; the call is synchronous, deterministic, and a few tens of
|
|
29
|
+
* microseconds, so the policy set answers as a plain `Rule`.
|
|
23
30
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
31
|
+
* The wasm module (about 12 MB on disk) is instantiated when the bindings
|
|
32
|
+
* load, which is why this engine lives in its own package: a deployment that
|
|
33
|
+
* evaluates out of process never carries it.
|
|
27
34
|
*
|
|
28
|
-
* It vouches for the revision it evaluated (`confirmsRevision
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* object the caller still holds; reading `source.revision` when answering would
|
|
34
|
-
* be reading "the current revision", which is a claim about the object and not
|
|
35
|
-
* about the policies that were compiled.
|
|
35
|
+
* It vouches for the revision it evaluated (`confirmsRevision`): the set is
|
|
36
|
+
* compiled here, under an id known to nothing else, so an answer through that
|
|
37
|
+
* id comes from the source `load` was handed and names that source's revision,
|
|
38
|
+
* read once at `load`. Reading `source.revision` when answering would describe
|
|
39
|
+
* the caller's object as it is now, not the policies that were compiled.
|
|
36
40
|
*/
|
|
37
41
|
export const cedarWasmEngine = {
|
|
38
42
|
name: CEDAR_WASM_ENGINE_NAME,
|
|
39
43
|
async: false,
|
|
40
44
|
confirmsRevision: true,
|
|
41
45
|
load(source) {
|
|
42
|
-
|
|
43
|
-
const answer = checkParsePolicySet({ staticPolicies: file.text });
|
|
44
|
-
if (answer.type === "failure") {
|
|
45
|
-
throw new CedarEngineError(`${file.source} failed to parse: ${details(answer.errors)}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
46
|
+
const policies = namePolicies(source.files, policiesOf);
|
|
48
47
|
// Captured with the compile below, not read per answer — see the doc comment.
|
|
49
48
|
const { revision } = source;
|
|
50
49
|
const policySetId = `auth.policy-verifier.cedar-wasm:${policySetCounter++}`;
|
|
51
|
-
const compiled = preparsePolicySet(policySetId, {
|
|
50
|
+
const compiled = preparsePolicySet(policySetId, {
|
|
51
|
+
staticPolicies: Object.fromEntries(policies.map((policy) => [policy.id, policy.text])),
|
|
52
|
+
});
|
|
52
53
|
if (compiled.type === "failure") {
|
|
53
54
|
throw new CedarEngineError(`policy set from ${source.description} failed to compile: ${details(compiled.errors)}`);
|
|
54
55
|
}
|
|
@@ -70,6 +71,30 @@ export const cedarWasmEngine = {
|
|
|
70
71
|
};
|
|
71
72
|
},
|
|
72
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* The policies of one file, as text, in the file's order. Cedar splits a file
|
|
76
|
+
* under positional ids — `policy0`, `policy1`… — and hands the parts back
|
|
77
|
+
* sorted by those ids as strings, so `policy10` comes before `policy2`; the
|
|
78
|
+
* order is undone here by sorting the same ids the same way. That sort is the
|
|
79
|
+
* pinned `@cedar-policy/cedar-wasm`'s (see package.json); the test of a file
|
|
80
|
+
* holding a dozen policies fails on a release that sorts otherwise.
|
|
81
|
+
*/
|
|
82
|
+
function policiesOf(file) {
|
|
83
|
+
const parts = policySetTextToParts(file.text);
|
|
84
|
+
if (parts.type === "failure") {
|
|
85
|
+
throw new CedarEngineError(`${file.source} failed to parse: ${details(parts.errors)}`);
|
|
86
|
+
}
|
|
87
|
+
if (parts.policy_templates.length > 0) {
|
|
88
|
+
// An unlinked template never applies, and this engine links none.
|
|
89
|
+
throw new CedarEngineError(`${file.source} holds a template — a policy with ?principal or ?resource never applies here, because nothing links it`);
|
|
90
|
+
}
|
|
91
|
+
const sorted = parts.policies.map((_, position) => `policy${position}`).sort();
|
|
92
|
+
const inFileOrder = new Array(parts.policies.length);
|
|
93
|
+
for (const [at, id] of sorted.entries()) {
|
|
94
|
+
inFileOrder[Number(id.slice("policy".length))] = parts.policies[at];
|
|
95
|
+
}
|
|
96
|
+
return inFileOrder;
|
|
97
|
+
}
|
|
73
98
|
function details(errors) {
|
|
74
99
|
return errors.map((error) => error.message).join("; ");
|
|
75
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth.policy-verifier.cedar-wasm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@cedar-policy/cedar-wasm": "4.13.0",
|
|
29
|
-
"@o3co/auth.policy-verifier.cedar": "0.
|
|
29
|
+
"@o3co/auth.policy-verifier.cedar": "0.16.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^26.6.
|
|
33
|
-
"@vitest/coverage-v8": "^5.0.
|
|
34
|
-
"vitest": "^5.0.
|
|
35
|
-
"@o3co/auth.policy-verifier.core": "0.
|
|
32
|
+
"@types/node": "^26.6.3",
|
|
33
|
+
"@vitest/coverage-v8": "^5.0.2",
|
|
34
|
+
"vitest": "^5.0.2",
|
|
35
|
+
"@o3co/auth.policy-verifier.core": "0.16.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"prebuild": "rimraf dist",
|