@intentius/chant-lexicon-cedar 0.124.31 → 0.124.32
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/dist/agentcore/embed.js +250 -0
- package/dist/agentcore/enforcement.js +81 -0
- package/dist/agentcore/scan.js +93 -0
- package/dist/avp/ambient.js +100 -0
- package/dist/avp/client.js +318 -0
- package/dist/avp/describe-resources.js +144 -0
- package/dist/avp/embed.js +125 -0
- package/dist/avp/live-export.js +151 -0
- package/dist/avp/ownership.js +144 -0
- package/dist/avp/statement.js +45 -0
- package/dist/avp/store.js +105 -0
- package/dist/avp/testdata/mock-transport.js +100 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +133 -0
- package/dist/codegen/emit.js +331 -0
- package/dist/codegen/generate-cli.js +18 -0
- package/dist/codegen/generate.js +122 -0
- package/dist/codegen/naming.js +90 -0
- package/dist/codegen/package.js +33 -0
- package/dist/commands.js +86 -0
- package/dist/composites/deny-by-default-set.js +51 -0
- package/dist/composites/index.js +9 -0
- package/dist/composites/owner-can-manage.js +48 -0
- package/dist/config.js +159 -0
- package/dist/coverage.js +112 -0
- package/dist/detect.js +61 -0
- package/dist/dogwood/cli.js +477 -0
- package/dist/dogwood/event-schema.js +219 -0
- package/dist/dogwood/index.js +32 -0
- package/dist/dogwood/macros.js +175 -0
- package/dist/dogwood/policy.js +58 -0
- package/dist/dogwood/replay-activity.js +313 -0
- package/dist/dogwood/replay-op.js +134 -0
- package/dist/dogwood/scan.js +234 -0
- package/dist/dogwood/serialize.js +199 -0
- package/dist/dogwood/temporal.js +354 -0
- package/dist/dogwood/trace.js +269 -0
- package/dist/dogwood/upstream.js +40 -0
- package/dist/dogwood/window.js +82 -0
- package/dist/gate-policy-pack.js +189 -0
- package/dist/gate-policy.js +168 -0
- package/dist/gate-schema.js +122 -0
- package/dist/generated/index.js +94 -0
- package/dist/generated/runtime.js +2 -0
- package/dist/import/adapter.js +54 -0
- package/dist/import/clause-text.js +166 -0
- package/dist/import/generator.js +178 -0
- package/dist/import/parser.js +273 -0
- package/dist/index.js +79 -0
- package/dist/init-templates.js +330 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +65 -0
- package/dist/lint/audit-lineage.js +23 -0
- package/dist/lint/post-synth/cedar-helpers.js +123 -0
- package/dist/lint/post-synth/cedc010.js +59 -0
- package/dist/lint/post-synth/cedc011.js +29 -0
- package/dist/lint/post-synth/cedc012.js +34 -0
- package/dist/lint/post-synth/cedc013.js +45 -0
- package/dist/lint/post-synth/cedc014.js +54 -0
- package/dist/lint/post-synth/cede010.js +51 -0
- package/dist/lint/post-synth/cede011.js +36 -0
- package/dist/lint/post-synth/ceds010.js +31 -0
- package/dist/lint/post-synth/ceds011.js +22 -0
- package/dist/lint/post-synth/ceds012.js +31 -0
- package/dist/lint/post-synth/dogwood-helpers.js +94 -0
- package/dist/lint/post-synth/dwdc010.js +38 -0
- package/dist/lint/post-synth/dwdc011.js +34 -0
- package/dist/lint/post-synth/dwdc012.js +24 -0
- package/dist/lint/post-synth/dwdc013.js +29 -0
- package/dist/lint/post-synth/dwde010.js +84 -0
- package/dist/lint/post-synth/dwde011.js +67 -0
- package/dist/lint/post-synth/dwds010.js +21 -0
- package/dist/lint/post-synth/index.js +36 -0
- package/dist/lint/post-synth/wasm-helpers.js +234 -0
- package/dist/lint/rules/index.js +9 -0
- package/dist/lint/rules/policy-shape.js +114 -0
- package/dist/lsp/completions.js +52 -0
- package/dist/lsp/hover.js +56 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +51 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/policy-coverage.js +134 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/package-cli.js +18 -0
- package/dist/plugin.js +304 -0
- package/dist/policy-text.js +117 -0
- package/dist/schema-artifact.js +66 -0
- package/dist/serializer.js +203 -0
- package/dist/spec/fetch.js +96 -0
- package/dist/spec/parse.js +164 -0
- package/dist/spec/pin.js +170 -0
- package/dist/spec/wasm.js +194 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate.js +74 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/coverage.ts +2 -2
- package/src/import/generator.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/spec/fetch.ts +2 -2
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
|
|
2
|
+
import { cedarSerializer } from "./serializer.js";
|
|
3
|
+
import { rules } from "./lint/rules/index.js";
|
|
4
|
+
import { postSynthChecks as cedarPostSynthChecks } from "./lint/post-synth/index.js";
|
|
5
|
+
import { cedarAuditCatalog } from "./lint/audit-catalog.js";
|
|
6
|
+
import { completions } from "./lsp/completions.js";
|
|
7
|
+
import { hover } from "./lsp/hover.js";
|
|
8
|
+
import { cedarMcpResources, cedarMcpTools } from "./mcp/index.js";
|
|
9
|
+
import { cedarInitTemplates } from "./init-templates.js";
|
|
10
|
+
import { cedarConfigSchema } from "./config.js";
|
|
11
|
+
import { detectTemplate as detectCedarTemplate } from "./detect.js";
|
|
12
|
+
import { CedarTemplateGenerator, CedarTemplateParser } from "./import/adapter.js";
|
|
13
|
+
import { AVP_OWNERSHIP_KEYS } from "./avp/ownership.js";
|
|
14
|
+
import { AVP_AMBIENT_KINDS } from "./avp/ambient.js";
|
|
15
|
+
import { cedarCommandGroup } from "./commands.js";
|
|
16
|
+
/**
|
|
17
|
+
* cedar lexicon plugin.
|
|
18
|
+
*
|
|
19
|
+
* Implements all required LexiconPlugin lifecycle methods.
|
|
20
|
+
*/
|
|
21
|
+
export const cedarPlugin = {
|
|
22
|
+
name: "cedar",
|
|
23
|
+
serializer: cedarSerializer,
|
|
24
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
25
|
+
async generate(options) {
|
|
26
|
+
// Cedar's codegen input is the project's own schema, so the `cedar` config
|
|
27
|
+
// namespace is read here rather than being decoration (#1650).
|
|
28
|
+
//
|
|
29
|
+
// The output goes into the *project* (#1696): `cedar.outDir`, else
|
|
30
|
+
// `src/generated/cedar` under the config's directory. Only when the project
|
|
31
|
+
// is this package itself does it land in `src/generated/`.
|
|
32
|
+
const { generate, resolveGeneratedDir, writeGeneratedFiles } = await import("./codegen/generate.js");
|
|
33
|
+
const { loadCedarProject } = await import("./config.js");
|
|
34
|
+
const { projectRoot, config } = await loadCedarProject(process.cwd());
|
|
35
|
+
const result = await generate({ ...options, projectRoot, config });
|
|
36
|
+
const outDir = resolveGeneratedDir({ projectRoot, config });
|
|
37
|
+
writeGeneratedFiles(result, outDir);
|
|
38
|
+
if (options?.verbose)
|
|
39
|
+
console.error(`cedar: wrote generated classes to ${outDir}`);
|
|
40
|
+
},
|
|
41
|
+
async validate(options) {
|
|
42
|
+
const { validate } = await import("./validate.js");
|
|
43
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
44
|
+
const result = await validate();
|
|
45
|
+
printValidationResult(result);
|
|
46
|
+
},
|
|
47
|
+
async coverage(options) {
|
|
48
|
+
const { analyzeCedarCoverage } = await import("./coverage.js");
|
|
49
|
+
const { resolveGeneratedDir } = await import("./codegen/generate.js");
|
|
50
|
+
const { loadCedarProject } = await import("./config.js");
|
|
51
|
+
const { projectRoot, config } = await loadCedarProject(process.cwd());
|
|
52
|
+
analyzeCedarCoverage({
|
|
53
|
+
projectRoot,
|
|
54
|
+
config,
|
|
55
|
+
// Coverage compares the project's schema with the project's generated
|
|
56
|
+
// artifacts, which live where generate put them (#1696).
|
|
57
|
+
generatedDir: resolveGeneratedDir({ projectRoot, config }),
|
|
58
|
+
verbose: options?.verbose,
|
|
59
|
+
minOverall: options?.minOverall,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
async package(options) {
|
|
63
|
+
const { packageLexicon } = await import("./codegen/package.js");
|
|
64
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
65
|
+
const { join, dirname } = await import("path");
|
|
66
|
+
const { fileURLToPath } = await import("url");
|
|
67
|
+
const { spec, stats } = await packageLexicon(options);
|
|
68
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
69
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
70
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
71
|
+
},
|
|
72
|
+
// ── Optional extensions ────────────────────────────────────
|
|
73
|
+
// #1697 — the project's `.cedarschema` joins the build as a `Cedar::Schema`
|
|
74
|
+
// entity, so it is emitted beside the policies and CEDE010 validates
|
|
75
|
+
// against it rather than reporting that it could not. Nothing is
|
|
76
|
+
// contributed when only the bundled default schema applies.
|
|
77
|
+
async buildRoots(ctx) {
|
|
78
|
+
const { schemaBuildRoot } = await import("./schema-artifact.js");
|
|
79
|
+
return schemaBuildRoot(ctx);
|
|
80
|
+
},
|
|
81
|
+
// `chant cedar generate` and `chant cedar coverage` (#1696, via #1078).
|
|
82
|
+
commands() {
|
|
83
|
+
return cedarCommandGroup();
|
|
84
|
+
},
|
|
85
|
+
lintRules() {
|
|
86
|
+
return rules;
|
|
87
|
+
},
|
|
88
|
+
postSynthChecks() {
|
|
89
|
+
// Auto-discovered from lint/post-synth/ and committed as a static barrel
|
|
90
|
+
// by `npm run generate:barrels` — adding a check is dropping a file.
|
|
91
|
+
return cedarPostSynthChecks;
|
|
92
|
+
},
|
|
93
|
+
auditCatalog() {
|
|
94
|
+
return cedarAuditCatalog;
|
|
95
|
+
},
|
|
96
|
+
skills: createSkillsLoader(import.meta.url, [
|
|
97
|
+
{
|
|
98
|
+
file: "chant-cedar-authoring.md",
|
|
99
|
+
name: "chant-cedar-authoring",
|
|
100
|
+
description: "Author Cedar policies as typed chant resources — schema to generated classes to .cedar and JSON outputs",
|
|
101
|
+
triggers: [
|
|
102
|
+
{ type: "file-pattern", value: "**/*.cedarschema" },
|
|
103
|
+
{ type: "file-pattern", value: "**/policies.ts" },
|
|
104
|
+
{ type: "context", value: "cedar policy" },
|
|
105
|
+
{ type: "context", value: "cedar schema" },
|
|
106
|
+
{ type: "context", value: "authorization policy" },
|
|
107
|
+
],
|
|
108
|
+
parameters: [],
|
|
109
|
+
examples: [
|
|
110
|
+
{
|
|
111
|
+
title: "Owner-scoped permit",
|
|
112
|
+
description: "Grant an action to the owner of a resource",
|
|
113
|
+
input: "Let users read documents they own",
|
|
114
|
+
output: `new Policy({ principal: { is: "App::User" }, action: { eq: ReadAction }, when: ["resource.owner == principal"] })`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
file: "chant-cedar-avp-embedding.md",
|
|
120
|
+
name: "chant-cedar-avp-embedding",
|
|
121
|
+
description: "Embed a typed cedar-lexicon policy into an AWS Verified Permissions policy resource instead of a hand-written string",
|
|
122
|
+
triggers: [
|
|
123
|
+
{ type: "context", value: "verified permissions" },
|
|
124
|
+
{ type: "context", value: "avp policy store" },
|
|
125
|
+
{ type: "context", value: "cedar deployment" },
|
|
126
|
+
{ type: "context", value: "cedar-agent" },
|
|
127
|
+
],
|
|
128
|
+
parameters: [],
|
|
129
|
+
examples: [
|
|
130
|
+
{
|
|
131
|
+
title: "Policy destined for a policy store",
|
|
132
|
+
input: "Deploy this Cedar policy to Verified Permissions",
|
|
133
|
+
output: "Emit the .cedar text and reference it — the typed statement seam lands with #1652",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
file: "chant-cedar-meta-policy.md",
|
|
139
|
+
name: "chant-cedar-meta-policy",
|
|
140
|
+
description: "Organizational policy over Cedar policy sets — the bare-permit wall, forbid conventions, and env-aware severity",
|
|
141
|
+
triggers: [
|
|
142
|
+
{ type: "context", value: "cedar lint" },
|
|
143
|
+
{ type: "context", value: "bare permit" },
|
|
144
|
+
{ type: "context", value: "policy review" },
|
|
145
|
+
{ type: "context", value: "meta-policy" },
|
|
146
|
+
],
|
|
147
|
+
parameters: [],
|
|
148
|
+
examples: [
|
|
149
|
+
{
|
|
150
|
+
title: "The bare-permit wall",
|
|
151
|
+
input: "Is `permit (principal, action, resource);` acceptable?",
|
|
152
|
+
output: "Warn in dev and staging; fail the build in prod",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
file: "chant-cedar-dogwood.md",
|
|
158
|
+
name: "chant-cedar-dogwood",
|
|
159
|
+
description: "Author dogwood temporal policies with the typed builders — parser primitives versus default-library macros, the callerPrincipal pin, the CLI-gated validation split, AgentCore embedding and trace replay",
|
|
160
|
+
triggers: [
|
|
161
|
+
{ type: "file-pattern", value: "**/*.dw" },
|
|
162
|
+
{ type: "file-pattern", value: "**/*.dwschema" },
|
|
163
|
+
{ type: "context", value: "dogwood" },
|
|
164
|
+
{ type: "context", value: "temporal policy" },
|
|
165
|
+
{ type: "context", value: "agentcore policy" },
|
|
166
|
+
{ type: "context", value: "rate limit policy" },
|
|
167
|
+
{ type: "context", value: "policy replay" },
|
|
168
|
+
],
|
|
169
|
+
parameters: [],
|
|
170
|
+
examples: [
|
|
171
|
+
{
|
|
172
|
+
title: "Approval before action",
|
|
173
|
+
description: "Allow a read only if a login happened inside the window",
|
|
174
|
+
input: "Only let them read if they logged in within the last hour",
|
|
175
|
+
output: `whenTemporal: [dogwood.formerly("1h", dogwood.predicate('Drupe::Action::"Login"', "response"))]`,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
title: "The macro caveat",
|
|
179
|
+
input: "Use count_within for a rate limit",
|
|
180
|
+
output: "count_within is a default-library macro a --macros caller replaces — emit the call and ship the definition, or build it from `count for … where`",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
]),
|
|
185
|
+
initTemplates(template) {
|
|
186
|
+
// The branch names are spelled out here rather than only in
|
|
187
|
+
// ./init-templates.ts because `chant dev check-lexicon` counts a lexicon's
|
|
188
|
+
// templates by scanning this file. The content lives next door.
|
|
189
|
+
if (template === "avp-embedding")
|
|
190
|
+
return cedarInitTemplates("avp-embedding");
|
|
191
|
+
if (template === "gateway-policy-set")
|
|
192
|
+
return cedarInitTemplates("gateway-policy-set");
|
|
193
|
+
return cedarInitTemplates(template);
|
|
194
|
+
},
|
|
195
|
+
mcpTools() {
|
|
196
|
+
return cedarMcpTools();
|
|
197
|
+
},
|
|
198
|
+
mcpResources() {
|
|
199
|
+
// `createCatalogResource` resolves the registry relative to the module URL
|
|
200
|
+
// it is handed, so it needs this file's — src/generated/, not src/mcp/.
|
|
201
|
+
return cedarMcpResources(import.meta.url);
|
|
202
|
+
},
|
|
203
|
+
detectTemplate(data) {
|
|
204
|
+
return detectCedarTemplate(data);
|
|
205
|
+
},
|
|
206
|
+
/** `.cedar` text and the JSON policy-set envelope, into the core IR (#1653). */
|
|
207
|
+
templateParser() {
|
|
208
|
+
return new CedarTemplateParser();
|
|
209
|
+
},
|
|
210
|
+
/** The IR back out as `new Policy({ … })` against the generated classes. */
|
|
211
|
+
templateGenerator() {
|
|
212
|
+
return new CedarTemplateGenerator();
|
|
213
|
+
},
|
|
214
|
+
completionProvider(ctx) {
|
|
215
|
+
return completions(ctx);
|
|
216
|
+
},
|
|
217
|
+
hoverProvider(ctx) {
|
|
218
|
+
return hover(ctx);
|
|
219
|
+
},
|
|
220
|
+
async docs(options) {
|
|
221
|
+
const { generateDocs } = await import("./codegen/docs.js");
|
|
222
|
+
return generateDocs(options);
|
|
223
|
+
},
|
|
224
|
+
/** The shape of the `cedar` key in `chant.config.ts` — see ./config.ts. */
|
|
225
|
+
configSchema: cedarConfigSchema,
|
|
226
|
+
/**
|
|
227
|
+
* The pinned upstream is the Cedar *grammar*, not a downloaded spec (#1650).
|
|
228
|
+
*
|
|
229
|
+
* `CEDAR_WASM_VERSION` is the package version the self-upgrade tooling bumps;
|
|
230
|
+
* `CEDAR_LANG_VERSION` beside it is the language version `generate()` asserts
|
|
231
|
+
* at runtime, since a package bump that leaves the language at 4.5 cannot
|
|
232
|
+
* change what parses. Both live in src/spec/pin.ts, which also carries the
|
|
233
|
+
* content pin over the bundled default schema.
|
|
234
|
+
*/
|
|
235
|
+
upstreamPin: {
|
|
236
|
+
file: "src/spec/pin.ts",
|
|
237
|
+
pattern: /export const CEDAR_WASM_VERSION\s*=\s*"([^"]+)"/,
|
|
238
|
+
replace: (v, line) => line.replace(/= "[^"]+"/, `= "${v}"`),
|
|
239
|
+
upstream: { owner: "cedar-policy", repo: "cedar", kind: "releases" },
|
|
240
|
+
},
|
|
241
|
+
// ── AVP lifecycle (#1652) ──────────────────────────────────────
|
|
242
|
+
/**
|
|
243
|
+
* AVP policy stores are taggable; the policies inside them are not, so the
|
|
244
|
+
* per-policy channel is a marker encoded into `Definition.Static.Description`
|
|
245
|
+
* and the store's tags are the coarse channel beside it. Only the two paths
|
|
246
|
+
* that actually read the description are declared (chant #1348) — the full
|
|
247
|
+
* design record is src/avp/OWNERSHIP.md.
|
|
248
|
+
*/
|
|
249
|
+
ownershipChannel: {
|
|
250
|
+
keys: AVP_OWNERSHIP_KEYS,
|
|
251
|
+
reads: ["describeResources", "exportResources"],
|
|
252
|
+
},
|
|
253
|
+
/**
|
|
254
|
+
* Observe the declared policy set against a live AVP policy store.
|
|
255
|
+
*
|
|
256
|
+
* The tri-state carries more weight here than on most substrates: a declared
|
|
257
|
+
* policy reported absent becomes a `create`, and creating a permit that
|
|
258
|
+
* already exists duplicates a grant. Every path that did not look says so.
|
|
259
|
+
*/
|
|
260
|
+
async describeResources(options) {
|
|
261
|
+
const { describeAvpResources } = await import("./avp/describe-resources.js");
|
|
262
|
+
return describeAvpResources({
|
|
263
|
+
environment: options.environment,
|
|
264
|
+
entityNames: options.entityNames,
|
|
265
|
+
entities: options.entities,
|
|
266
|
+
...(options.owned !== undefined ? { owned: options.owned } : {}),
|
|
267
|
+
...(options.region ? { client: { region: options.region } } : {}),
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
/** The one kind this lexicon deploys, and therefore the one it can enumerate. */
|
|
271
|
+
ambientKinds() {
|
|
272
|
+
return [...AVP_AMBIENT_KINDS];
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* Policies in the store that nothing declares — a standing grant nobody in
|
|
276
|
+
* the source tree can see. For an authorization store this is a security
|
|
277
|
+
* finding rather than housekeeping.
|
|
278
|
+
*/
|
|
279
|
+
async observeAmbient(options) {
|
|
280
|
+
const { observeAvpAmbient } = await import("./avp/ambient.js");
|
|
281
|
+
return observeAvpAmbient({
|
|
282
|
+
environment: options.environment,
|
|
283
|
+
kinds: options.kinds,
|
|
284
|
+
observed: options.observed,
|
|
285
|
+
...(options.region ? { client: { region: options.region } } : {}),
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
/**
|
|
289
|
+
* Full-fidelity read of the live policies, as import IR. Server-written
|
|
290
|
+
* fields are stripped to the declared shape unless `verbatim` is set — see
|
|
291
|
+
* the note in src/avp/live-export.ts for what counts as server-written when
|
|
292
|
+
* the only thing a user authors is the statement.
|
|
293
|
+
*/
|
|
294
|
+
async exportResources(options) {
|
|
295
|
+
const { exportAvpResources } = await import("./avp/live-export.js");
|
|
296
|
+
return exportAvpResources({
|
|
297
|
+
environment: options.environment,
|
|
298
|
+
...(options.selector ? { selector: options.selector } : {}),
|
|
299
|
+
...(options.owned !== undefined ? { owned: options.owned } : {}),
|
|
300
|
+
...(options.verbatim !== undefined ? { verbatim: options.verbatim } : {}),
|
|
301
|
+
...(options.region ? { client: { region: options.region } } : {}),
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar policy-text rendering, shared by the `.cedar` serializer, the AVP
|
|
3
|
+
* statement renderer, and the dogwood dialect's `.dw` leg.
|
|
4
|
+
*
|
|
5
|
+
* Upstream's dogwood policy grammar is Cedar's shape — `annotation* effect
|
|
6
|
+
* "(" scope ")" cond* ";"` — and only the `cond` rule differs, gaining
|
|
7
|
+
* `guardrails { … }` and `temporal { … }` forms. So everything above the first
|
|
8
|
+
* clause is common, and it lives here rather than being copied into
|
|
9
|
+
* `./dogwood/serialize.ts`, where a change to how a scope constraint is
|
|
10
|
+
* written would have had to be made twice.
|
|
11
|
+
*
|
|
12
|
+
* `resolvePolicyId` is here for the same reason it was extracted from
|
|
13
|
+
* `serialize()` by #1652: it is the only rule linking a chant entity to a
|
|
14
|
+
* policy id, the AVP observation matches against it, and the `.dw` leg has to
|
|
15
|
+
* derive ids the same way the `.cedar` leg does. One copy, three callers.
|
|
16
|
+
*
|
|
17
|
+
* This module is a leaf on purpose: `./serializer.ts` and `./dogwood/*` both
|
|
18
|
+
* import it, and neither imports the other's rendering.
|
|
19
|
+
*/
|
|
20
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
21
|
+
/** A plain object — not null, not an array. */
|
|
22
|
+
export function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
/** The `props` of a resource-kind Declarable, or `{}` for anything else. */
|
|
26
|
+
export function getProps(entity) {
|
|
27
|
+
if (isResourceDeclarable(entity) && isRecord(entity.props)) {
|
|
28
|
+
return entity.props;
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
/** Escape a string for a double-quoted Cedar literal. */
|
|
33
|
+
export function escapeCedarString(value) {
|
|
34
|
+
return value
|
|
35
|
+
.replace(/\\/g, "\\\\")
|
|
36
|
+
.replace(/"/g, '\\"')
|
|
37
|
+
.replace(/\n/g, "\\n")
|
|
38
|
+
.replace(/\r/g, "\\r")
|
|
39
|
+
.replace(/\t/g, "\\t");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Derive a policy id from a logical name: `allowAdminRead` → `allow-admin-read`.
|
|
43
|
+
* Cedar ids are free-form strings; kebab-case keeps them readable in the
|
|
44
|
+
* `@id` annotation and stable across a rename-free refactor.
|
|
45
|
+
*/
|
|
46
|
+
export function policyIdFromLogicalName(name) {
|
|
47
|
+
return name
|
|
48
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
49
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
50
|
+
.replace(/[\s_]+/g, "-")
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The Cedar id for a policy: an explicit `annotations.id` when the author gave
|
|
55
|
+
* one, else derived from the logical name.
|
|
56
|
+
*
|
|
57
|
+
* This is the only rule that links a chant entity to a policy in a live AVP
|
|
58
|
+
* store (#1652) — the observation resolves the same id from the same props and
|
|
59
|
+
* matches it against the `@id` annotation the statement carries — and it is
|
|
60
|
+
* also what gives a `.dw` policy the id its `.cedar` sibling would have had.
|
|
61
|
+
* Two copies of this rule would be a mapping that drifts silently.
|
|
62
|
+
*/
|
|
63
|
+
export function resolvePolicyId(logicalName, props) {
|
|
64
|
+
const explicit = isRecord(props.annotations) ? props.annotations.id : undefined;
|
|
65
|
+
return typeof explicit === "string" && explicit.length > 0
|
|
66
|
+
? explicit
|
|
67
|
+
: policyIdFromLogicalName(logicalName);
|
|
68
|
+
}
|
|
69
|
+
function refText(value) {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
/** `principal`, `principal == User::"alice"`, `action in [ … ]`, `resource is Photo`. */
|
|
73
|
+
export function renderScope(variable, scope) {
|
|
74
|
+
if (!isRecord(scope))
|
|
75
|
+
return variable;
|
|
76
|
+
const parts = [variable];
|
|
77
|
+
if (typeof scope.is === "string")
|
|
78
|
+
parts.push(`is ${scope.is}`);
|
|
79
|
+
if (scope.eq !== undefined)
|
|
80
|
+
parts.push(`== ${refText(scope.eq)}`);
|
|
81
|
+
if (scope.in !== undefined) {
|
|
82
|
+
parts.push(Array.isArray(scope.in)
|
|
83
|
+
? `in [${scope.in.map(refText).join(", ")}]`
|
|
84
|
+
: `in ${refText(scope.in)}`);
|
|
85
|
+
}
|
|
86
|
+
return parts.join(" ");
|
|
87
|
+
}
|
|
88
|
+
/** A `when`/`unless` prop as a list of clause bodies — one string, or several. */
|
|
89
|
+
export function conditionStrings(value) {
|
|
90
|
+
if (value === undefined || value === null)
|
|
91
|
+
return [];
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value.map(refText);
|
|
94
|
+
return [refText(value)];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Everything above the first clause: the annotations, the effect, and the
|
|
98
|
+
* three scope positions.
|
|
99
|
+
*/
|
|
100
|
+
export function renderPolicyHead(id, props) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
// @id first, then the author's own annotations in declaration order.
|
|
103
|
+
const annotations = isRecord(props.annotations) ? props.annotations : {};
|
|
104
|
+
lines.push(`@id("${escapeCedarString(id)}")`);
|
|
105
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
106
|
+
if (key === "id" || value === undefined || value === null)
|
|
107
|
+
continue;
|
|
108
|
+
lines.push(`@${key}("${escapeCedarString(refText(value))}")`);
|
|
109
|
+
}
|
|
110
|
+
const effect = props.effect === "forbid" ? "forbid" : "permit";
|
|
111
|
+
lines.push(`${effect} (`);
|
|
112
|
+
lines.push(` ${renderScope("principal", props.principal)},`);
|
|
113
|
+
lines.push(` ${renderScope("action", props.action)},`);
|
|
114
|
+
lines.push(` ${renderScope("resource", props.resource)}`);
|
|
115
|
+
lines.push(")");
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project schema as a build artifact (#1697).
|
|
3
|
+
*
|
|
4
|
+
* CEDE010 can only validate a policy set against a schema the build emitted
|
|
5
|
+
* beside it, and until now nothing emitted one: the `.cedarschema` drove
|
|
6
|
+
* codegen and then stayed in the source tree, so every consumer build got the
|
|
7
|
+
* parse-only advisory. This module closes that gap from two directions.
|
|
8
|
+
*
|
|
9
|
+
* `Schema` is a declarable a project can author by hand when it wants a
|
|
10
|
+
* particular filename or a schema that is not the one in `chant.config.ts`.
|
|
11
|
+
* `schemaBuildRoot` is the automatic path: the plugin's `buildRoots` hook
|
|
12
|
+
* contributes one `Schema` entity per build, holding the resolved project
|
|
13
|
+
* schema, so a project with a `cedar.schema` gets `dist/schema.cedarschema`
|
|
14
|
+
* and full validation without declaring anything.
|
|
15
|
+
*
|
|
16
|
+
* The bundled default schema is never emitted. A project without its own
|
|
17
|
+
* schema has policies the default may well reject, and CEDE010 failing a
|
|
18
|
+
* build against a schema the project never wrote would be a worse surprise
|
|
19
|
+
* than the advisory it replaces.
|
|
20
|
+
*/
|
|
21
|
+
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
22
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
23
|
+
import { getProps } from "./policy-text.js";
|
|
24
|
+
import { resolveSchemaPath } from "./spec/fetch.js";
|
|
25
|
+
import { readFileSync } from "fs";
|
|
26
|
+
import { basename } from "path";
|
|
27
|
+
/** The entity type of a {@link Schema}. */
|
|
28
|
+
export const CEDAR_SCHEMA_TYPE = "Cedar::Schema";
|
|
29
|
+
/** The logical name the build-root contribution uses. */
|
|
30
|
+
export const CEDAR_SCHEMA_ENTITY_NAME = "cedarSchema";
|
|
31
|
+
/** Filename the schema is emitted under when `filename` is unset. */
|
|
32
|
+
export const CEDAR_SCHEMA_FILENAME = "schema.cedarschema";
|
|
33
|
+
/** A `.cedarschema` emitted with the policy set, so post-synth validation has something to validate against. */
|
|
34
|
+
export const Schema = createResource(CEDAR_SCHEMA_TYPE, "cedar", {});
|
|
35
|
+
/** Every `Cedar::Schema` in the entity map, with its props read. */
|
|
36
|
+
export function schemaEntities(entities) {
|
|
37
|
+
const files = [];
|
|
38
|
+
for (const [name, entity] of entities) {
|
|
39
|
+
if (isPropertyDeclarable(entity))
|
|
40
|
+
continue;
|
|
41
|
+
if (entity.entityType !== CEDAR_SCHEMA_TYPE)
|
|
42
|
+
continue;
|
|
43
|
+
const props = getProps(entity);
|
|
44
|
+
if (typeof props.text !== "string")
|
|
45
|
+
continue;
|
|
46
|
+
files.push({
|
|
47
|
+
name,
|
|
48
|
+
filename: typeof props.filename === "string" ? props.filename : CEDAR_SCHEMA_FILENAME,
|
|
49
|
+
text: props.text.endsWith("\n") ? props.text : props.text + "\n",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The `buildRoots` contribution: the project's resolved schema as one
|
|
56
|
+
* {@link Schema} entity, or nothing when only the bundled default applies.
|
|
57
|
+
*/
|
|
58
|
+
export async function schemaBuildRoot(ctx) {
|
|
59
|
+
const config = ctx.config.cedar ?? {};
|
|
60
|
+
const source = resolveSchemaPath({ projectRoot: ctx.projectRoot, config });
|
|
61
|
+
if (source.isDefault)
|
|
62
|
+
return { entities: new Map() };
|
|
63
|
+
const text = readFileSync(source.path, "utf-8");
|
|
64
|
+
const entity = new Schema({ text, filename: basename(source.path) });
|
|
65
|
+
return { entities: new Map([[CEDAR_SCHEMA_ENTITY_NAME, entity]]) };
|
|
66
|
+
}
|