@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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AVP read transport (#1652).
|
|
3
|
+
*
|
|
4
|
+
* Amazon Verified Permissions speaks AWS JSON 1.0 — a POST of a JSON body with
|
|
5
|
+
* `x-amz-target: VerifiedPermissions.<Operation>` — so this is the same shape
|
|
6
|
+
* as `lexicons/aws/src/api/read-client.ts`'s Cloud Control half, pointed at a
|
|
7
|
+
* different service. That file is the precedent for the decision this module
|
|
8
|
+
* embodies: **no AWS SDK dependency**. The repo has none anywhere (the aws
|
|
9
|
+
* lexicon's runtime deps are `fflate` and `js-yaml`), and adding
|
|
10
|
+
* `@aws-sdk/client-verifiedpermissions` — 30-odd transitive packages — to the
|
|
11
|
+
* cedar lexicon so that one reader can issue three calls would put an AWS SDK
|
|
12
|
+
* in the dependency tree of a lexicon whose whole premise is that Cedar is
|
|
13
|
+
* vendor-neutral.
|
|
14
|
+
*
|
|
15
|
+
* ## Signing
|
|
16
|
+
*
|
|
17
|
+
* SigV4 lives once, in `lexicons/aws/src/api/sigv4.ts` (#1686). This module
|
|
18
|
+
* does not import it. A cedar → aws dependency edge would make the vendor-
|
|
19
|
+
* neutral lexicon unbuildable without the AWS one, which is the same rule that
|
|
20
|
+
* kept `src/avp/embed.ts` free of the aws lexicon: the seam is the data shape.
|
|
21
|
+
* So the signer arrives as a function on {@link AvpClientOptions}, typed here
|
|
22
|
+
* structurally against what the aws lexicon exports, and a project holding both
|
|
23
|
+
* lexicons wires them in one line:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { signRequest } from "@intentius/chant-lexicon-aws";
|
|
27
|
+
* import { describeAvpResources } from "@intentius/chant-lexicon-cedar";
|
|
28
|
+
*
|
|
29
|
+
* await describeAvpResources({
|
|
30
|
+
* environment,
|
|
31
|
+
* entityNames,
|
|
32
|
+
* entities,
|
|
33
|
+
* client: { region: "us-west-2", signer: signRequest },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Three cases go out unsigned, byte for byte as they did before signing
|
|
38
|
+
* existed — the credential scope of {@link regionScope} and nothing more:
|
|
39
|
+
*
|
|
40
|
+
* - **No signer.** The default, and the reason `credentialsAvailable` still
|
|
41
|
+
* gates the readers: a caller that wires nothing in is still an
|
|
42
|
+
* emulator-and-test transport, and real AWS still rejects it.
|
|
43
|
+
* - **No credentials.** A signer with nothing to sign with does not run.
|
|
44
|
+
* - **An endpoint override.** An emulator does not verify signatures, and
|
|
45
|
+
* signing against one would make every local lane need credentials to read
|
|
46
|
+
* what it just deployed. `signEndpointOverride` opts back in for an
|
|
47
|
+
* override that *is* real AWS — a VPC endpoint, a signing proxy.
|
|
48
|
+
*
|
|
49
|
+
* What counts as an override is {@link resolveEndpointOverride}: the
|
|
50
|
+
* `endpoint` option, else `AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS`, else
|
|
51
|
+
* `AWS_ENDPOINT_URL` — the AWS SDK's own precedence, and the same rule the aws
|
|
52
|
+
* lexicon's `api/read-client.ts` applies to every transport it owns (#1694).
|
|
53
|
+
* It is restated here rather than imported for the reason above: cedar does
|
|
54
|
+
* not depend on the aws lexicon, and a lexicon build compiles against the
|
|
55
|
+
* published core, so a helper hoisted into core would reach neither lexicon
|
|
56
|
+
* until the next core release.
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_REGION = "us-east-1";
|
|
59
|
+
const TARGET_PREFIX = "VerifiedPermissions";
|
|
60
|
+
const SERVICE = "verifiedpermissions";
|
|
61
|
+
const defaultHttp = async (url, init, signal) => {
|
|
62
|
+
const res = await fetch(url, { method: "POST", headers: init.headers, body: init.body, signal });
|
|
63
|
+
return { status: res.status, text: await res.text() };
|
|
64
|
+
};
|
|
65
|
+
/** A failed AVP read, carrying enough to classify it without matching on prose. */
|
|
66
|
+
export class AvpReadError extends Error {
|
|
67
|
+
constructor(message, status,
|
|
68
|
+
/** The service's own error code (`ResourceNotFoundException`, `AccessDeniedException`, …). */
|
|
69
|
+
code) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.code = code;
|
|
73
|
+
this.name = "AvpReadError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Credentials for a request: explicit → environment → absent.
|
|
78
|
+
*
|
|
79
|
+
* The same rule the aws lexicon's `resolveCredentials` applies, and for the same
|
|
80
|
+
* reasons: an injected resolver is authoritative and its refusal is not
|
|
81
|
+
* second-guessed against the environment, and a half-set environment is absent
|
|
82
|
+
* rather than a signature that cannot verify. A caller with a profile file, IMDS
|
|
83
|
+
* or a container credential endpoint resolves those itself and passes the result
|
|
84
|
+
* in — that is what the resolver form is for.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveAvpCredentials(source, env = process.env) {
|
|
87
|
+
if (typeof source === "function")
|
|
88
|
+
return source();
|
|
89
|
+
if (source)
|
|
90
|
+
return source;
|
|
91
|
+
const accessKeyId = env.AWS_ACCESS_KEY_ID;
|
|
92
|
+
const secretAccessKey = env.AWS_SECRET_ACCESS_KEY;
|
|
93
|
+
if (!accessKeyId || !secretAccessKey)
|
|
94
|
+
return undefined;
|
|
95
|
+
return {
|
|
96
|
+
accessKeyId,
|
|
97
|
+
secretAccessKey,
|
|
98
|
+
...(env.AWS_SESSION_TOKEN ? { sessionToken: env.AWS_SESSION_TOKEN } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Service host for AVP, honouring an endpoint override. */
|
|
102
|
+
export function avpUrl(endpoint, region = DEFAULT_REGION) {
|
|
103
|
+
return `${(endpoint ?? `https://${SERVICE}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
|
|
104
|
+
}
|
|
105
|
+
/** The service-specific endpoint variable the AWS SDK reads for Verified Permissions. */
|
|
106
|
+
const SERVICE_ENDPOINT_ENV_VAR = "AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS";
|
|
107
|
+
/**
|
|
108
|
+
* The one rule for what an endpoint override is (#1694): the `endpoint`
|
|
109
|
+
* option when given, else `AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS`, else the
|
|
110
|
+
* ambient `AWS_ENDPOINT_URL`. The result is both the target and, through
|
|
111
|
+
* {@link requestHeaders}, the fact that decides whether the request is signed.
|
|
112
|
+
* Returns `undefined` for real AWS. Mirrors the aws lexicon's
|
|
113
|
+
* `resolveEndpointOverride` for this one service.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveEndpointOverride(endpoint, env = process.env) {
|
|
116
|
+
return endpoint || env[SERVICE_ENDPOINT_ENV_VAR] || env.AWS_ENDPOINT_URL || undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Whether this process has anything to authenticate with.
|
|
120
|
+
*
|
|
121
|
+
* An endpoint override counts: an emulator is reached without credentials, and
|
|
122
|
+
* refusing to look at one because `AWS_ACCESS_KEY_ID` is unset would make the
|
|
123
|
+
* local lanes unobservable. Everything else is the ordinary AWS credential
|
|
124
|
+
* surface, minus the instance-metadata path this transport cannot use anyway.
|
|
125
|
+
*/
|
|
126
|
+
export function credentialsAvailable(env = process.env) {
|
|
127
|
+
return Boolean(resolveEndpointOverride(undefined, env) ||
|
|
128
|
+
env.AWS_ACCESS_KEY_ID ||
|
|
129
|
+
env.AWS_PROFILE ||
|
|
130
|
+
env.AWS_SESSION_TOKEN ||
|
|
131
|
+
env.AWS_ROLE_ARN ||
|
|
132
|
+
env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||
|
|
133
|
+
env.AWS_WEB_IDENTITY_TOKEN_FILE);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The credential scope header.
|
|
137
|
+
*
|
|
138
|
+
* This is NOT SigV4 — the signature is a placeholder, exactly as in the aws
|
|
139
|
+
* lexicon's read client, and it must not be mistaken for a signed read path. It
|
|
140
|
+
* carries the region so that an endpoint override (one host for every region)
|
|
141
|
+
* still reaches the right one, and it is only ever sent when no signature was
|
|
142
|
+
* produced; the moment one is, {@link requestHeaders} sends that instead.
|
|
143
|
+
*/
|
|
144
|
+
function regionScope(region, env) {
|
|
145
|
+
if (!region)
|
|
146
|
+
return {};
|
|
147
|
+
const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
148
|
+
const key = env.AWS_ACCESS_KEY_ID || "chant";
|
|
149
|
+
return {
|
|
150
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${SERVICE}/aws4_request, ` +
|
|
151
|
+
"SignedHeaders=host, Signature=unsigned",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function isRecord(value) {
|
|
155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The headers one request goes out with — signed when a signer was injected,
|
|
159
|
+
* something resolved to sign with, and the target is real AWS; scope-only in
|
|
160
|
+
* every other case, which is the path this transport had before signing.
|
|
161
|
+
*
|
|
162
|
+
* Signing needs a region even when the caller named none, because the scope
|
|
163
|
+
* string has a slot for one; it borrows the same default {@link avpUrl} used to
|
|
164
|
+
* build the host, so the signature agrees with the endpoint it is sent to.
|
|
165
|
+
*/
|
|
166
|
+
function requestHeaders(operation, url, body, endpoint, options, env) {
|
|
167
|
+
const base = {
|
|
168
|
+
"content-type": "application/x-amz-json-1.0",
|
|
169
|
+
"x-amz-target": `${TARGET_PREFIX}.${operation}`,
|
|
170
|
+
};
|
|
171
|
+
const signer = options.signer;
|
|
172
|
+
const credentials = signer ? resolveAvpCredentials(options.credentials, env) : undefined;
|
|
173
|
+
if (!signer || !credentials || (endpoint && options.signEndpointOverride !== true)) {
|
|
174
|
+
return { ...base, ...regionScope(options.region, env) };
|
|
175
|
+
}
|
|
176
|
+
return signer({
|
|
177
|
+
method: "POST",
|
|
178
|
+
url,
|
|
179
|
+
headers: base,
|
|
180
|
+
body,
|
|
181
|
+
service: SERVICE,
|
|
182
|
+
region: options.region ?? DEFAULT_REGION,
|
|
183
|
+
credentials,
|
|
184
|
+
...(options.now ? { now: options.now } : {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** One AVP call. Throws {@link AvpReadError} carrying the service's `__type`. */
|
|
188
|
+
export async function avpCall(operation, payload, options = {}) {
|
|
189
|
+
const env = options.env ?? process.env;
|
|
190
|
+
const http = options.http ?? defaultHttp;
|
|
191
|
+
const endpoint = resolveEndpointOverride(options.endpoint, env);
|
|
192
|
+
const url = avpUrl(endpoint, options.region);
|
|
193
|
+
const payloadJson = JSON.stringify(payload);
|
|
194
|
+
const res = await http(url, {
|
|
195
|
+
headers: requestHeaders(operation, url, payloadJson, endpoint, options, env),
|
|
196
|
+
body: payloadJson,
|
|
197
|
+
}, options.signal);
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(res.text);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new AvpReadError(`unparseable ${operation} response`, res.status);
|
|
204
|
+
}
|
|
205
|
+
const body = isRecord(parsed) ? parsed : {};
|
|
206
|
+
const type = typeof body.__type === "string" ? body.__type.split("#").pop() : undefined;
|
|
207
|
+
if (type || res.status >= 400) {
|
|
208
|
+
const message = typeof body.message === "string" ? body.message : `${operation} failed with HTTP ${res.status}`;
|
|
209
|
+
throw new AvpReadError(message, res.status, type);
|
|
210
|
+
}
|
|
211
|
+
return body;
|
|
212
|
+
}
|
|
213
|
+
function summaryOf(raw, fallbackStore) {
|
|
214
|
+
const definition = isRecord(raw.definition) ? raw.definition : {};
|
|
215
|
+
const staticDef = isRecord(definition.static) ? definition.static : {};
|
|
216
|
+
const description = typeof staticDef.description === "string" ? staticDef.description : undefined;
|
|
217
|
+
return {
|
|
218
|
+
policyStoreId: typeof raw.policyStoreId === "string" ? raw.policyStoreId : fallbackStore,
|
|
219
|
+
policyId: typeof raw.policyId === "string" ? raw.policyId : "",
|
|
220
|
+
policyType: typeof raw.policyType === "string" ? raw.policyType : "STATIC",
|
|
221
|
+
...(description !== undefined ? { description } : {}),
|
|
222
|
+
...(typeof raw.createdDate === "string" ? { createdDate: raw.createdDate } : {}),
|
|
223
|
+
...(typeof raw.lastUpdatedDate === "string" ? { lastUpdatedDate: raw.lastUpdatedDate } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** `ListPolicies` for one store, paginated to exhaustion. */
|
|
227
|
+
export async function listPolicies(policyStoreId, options = {}) {
|
|
228
|
+
const out = [];
|
|
229
|
+
let nextToken;
|
|
230
|
+
do {
|
|
231
|
+
const body = await avpCall("ListPolicies", { policyStoreId, ...(nextToken ? { nextToken } : {}) }, options);
|
|
232
|
+
const rows = Array.isArray(body.policies) ? body.policies : [];
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
if (isRecord(row))
|
|
235
|
+
out.push(summaryOf(row, policyStoreId));
|
|
236
|
+
}
|
|
237
|
+
nextToken = typeof body.nextToken === "string" && body.nextToken.length > 0 ? body.nextToken : undefined;
|
|
238
|
+
} while (nextToken);
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `GetPolicy` — the full record for one policy, statement included.
|
|
243
|
+
*
|
|
244
|
+
* A template-linked policy has no `definition.static`, so its statement comes
|
|
245
|
+
* back empty; callers treat an empty statement as "not a static policy this
|
|
246
|
+
* lexicon authored" rather than as a parse failure.
|
|
247
|
+
*/
|
|
248
|
+
export async function getPolicy(policyStoreId, policyId, options = {}) {
|
|
249
|
+
const body = await avpCall("GetPolicy", { policyStoreId, policyId }, options);
|
|
250
|
+
const definition = isRecord(body.definition) ? body.definition : {};
|
|
251
|
+
const staticDef = isRecord(definition.static) ? definition.static : {};
|
|
252
|
+
return {
|
|
253
|
+
...summaryOf(body, policyStoreId),
|
|
254
|
+
policyId: typeof body.policyId === "string" ? body.policyId : policyId,
|
|
255
|
+
statement: typeof staticDef.statement === "string" ? staticDef.statement : "",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** `GetPolicyStore` — used to resolve the store ARN the tag read needs. */
|
|
259
|
+
export async function getPolicyStore(policyStoreId, options = {}) {
|
|
260
|
+
const body = await avpCall("GetPolicyStore", { policyStoreId }, options);
|
|
261
|
+
return {
|
|
262
|
+
policyStoreId: typeof body.policyStoreId === "string" ? body.policyStoreId : policyStoreId,
|
|
263
|
+
...(typeof body.arn === "string" ? { arn: body.arn } : {}),
|
|
264
|
+
...(typeof body.description === "string" ? { description: body.description } : {}),
|
|
265
|
+
...(typeof body.createdDate === "string" ? { createdDate: body.createdDate } : {}),
|
|
266
|
+
...(typeof body.lastUpdatedDate === "string" ? { lastUpdatedDate: body.lastUpdatedDate } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** `ListTagsForResource` — the store-level half of the ownership channel. */
|
|
270
|
+
export async function listStoreTags(resourceArn, options = {}) {
|
|
271
|
+
const body = await avpCall("ListTagsForResource", { resourceArn }, options);
|
|
272
|
+
const tags = isRecord(body.tags) ? body.tags : {};
|
|
273
|
+
const out = {};
|
|
274
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
275
|
+
if (typeof value === "string")
|
|
276
|
+
out[key] = value;
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/* ── Classification ───────────────────────────────────────────────────────── */
|
|
281
|
+
/** True when the failure means the store itself is not there. */
|
|
282
|
+
export function storeDoesNotExist(err) {
|
|
283
|
+
if (!(err instanceof AvpReadError))
|
|
284
|
+
return false;
|
|
285
|
+
return err.code === "ResourceNotFoundException" || /not\s*found|does not exist/i.test(err.message);
|
|
286
|
+
}
|
|
287
|
+
const CREDENTIAL_PATTERN = /credential|token|expired|AccessDenied|not authorized|Unauthorized|UnrecognizedClient|InvalidSignature/i;
|
|
288
|
+
/**
|
|
289
|
+
* Map a transport failure onto the closed {@link UnobservedReason} set.
|
|
290
|
+
*
|
|
291
|
+
* Anything that is not demonstrably a credential problem is `read-failed`,
|
|
292
|
+
* because guessing wider would let a throttle or a DNS failure masquerade as a
|
|
293
|
+
* verdict about the estate.
|
|
294
|
+
*/
|
|
295
|
+
export function classifyAvpFailure(err) {
|
|
296
|
+
const code = err instanceof AvpReadError && err.code ? `${err.code}: ` : "";
|
|
297
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
298
|
+
const detail = `${code}${message}`;
|
|
299
|
+
return {
|
|
300
|
+
reason: CREDENTIAL_PATTERN.test(detail) ? "no-credentials" : "read-failed",
|
|
301
|
+
detail,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The address a read was issued against, for the observation's `queried` map
|
|
306
|
+
* (chant #1620).
|
|
307
|
+
*
|
|
308
|
+
* An absence is only as trustworthy as the address behind it, and an AVP read
|
|
309
|
+
* has three ways to be pointed at the wrong place: the region, the store id,
|
|
310
|
+
* and the `@id` annotation the policy is matched on. All three are in here.
|
|
311
|
+
*/
|
|
312
|
+
export function policyAddress(policyStoreId, cedarPolicyId, region) {
|
|
313
|
+
return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policy?@id=${encodeURIComponent(cedarPolicyId)}`;
|
|
314
|
+
}
|
|
315
|
+
/** The address of the store-wide enumeration itself. */
|
|
316
|
+
export function storeAddress(policyStoreId, region) {
|
|
317
|
+
return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policies`;
|
|
318
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `describeResources()` against an AVP policy store (#1652).
|
|
3
|
+
*
|
|
4
|
+
* The tri-state matters more here than it does for most substrates. A Cedar
|
|
5
|
+
* policy that chant declares and the store does not have is a *missing
|
|
6
|
+
* authorization rule*: reporting it absent proposes a create, which is right.
|
|
7
|
+
* Reporting it absent because the store id was wrong, or because the read
|
|
8
|
+
* failed, proposes creating a permit that already exists — duplicating a grant.
|
|
9
|
+
* So every path that did not actually look says so.
|
|
10
|
+
*
|
|
11
|
+
* Built on core's observer harness (`observeEntities`), which owns the shape:
|
|
12
|
+
* bind-or-not-observe-all with a typed reason, bounded concurrency, a per-entity
|
|
13
|
+
* throw degrading to `read-failed` rather than a silent absence, and collection
|
|
14
|
+
* of the `queried` addresses.
|
|
15
|
+
*/
|
|
16
|
+
import { observation, observeEntities, unobservedAll, } from "@intentius/chant/observation";
|
|
17
|
+
import { CEDAR_POLICY_TYPE, resolvePolicyId } from "../serializer.js";
|
|
18
|
+
import { classifyAvpFailure, credentialsAvailable, policyAddress, storeAddress, storeDoesNotExist, } from "./client.js";
|
|
19
|
+
import { ownershipFromDescription } from "./ownership.js";
|
|
20
|
+
import { indexByCedarId, loadLivePolicies, resolvePolicyStoreId, } from "./store.js";
|
|
21
|
+
/** A bind failure that already knows its verdict. */
|
|
22
|
+
class AvpBindFailure extends Error {
|
|
23
|
+
constructor(reason, detail) {
|
|
24
|
+
super(detail);
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
this.detail = detail;
|
|
27
|
+
this.name = "AvpBindFailure";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function metadataFor(policy, ownership) {
|
|
31
|
+
return {
|
|
32
|
+
type: CEDAR_POLICY_TYPE,
|
|
33
|
+
physicalId: policy.policyId,
|
|
34
|
+
// AVP has no lifecycle state on a policy: it exists or it does not. The
|
|
35
|
+
// policy type is the most useful thing to show, and a template-linked
|
|
36
|
+
// policy differing from a static one IS drift worth seeing.
|
|
37
|
+
status: policy.policyType || "STATIC",
|
|
38
|
+
...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
|
|
39
|
+
ownership,
|
|
40
|
+
attributes: {
|
|
41
|
+
policyStoreId: policy.policyStoreId,
|
|
42
|
+
policyId: policy.policyId,
|
|
43
|
+
policyType: policy.policyType,
|
|
44
|
+
...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
|
|
45
|
+
...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
|
|
46
|
+
...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Observe the declared policy set against a live AVP store.
|
|
52
|
+
*
|
|
53
|
+
* Never throws for a transport or binding problem — a throw is the
|
|
54
|
+
* whole-lexicon failure and core would mark every entity `read-failed`, which
|
|
55
|
+
* is a worse-shaped version of what this returns directly with the right
|
|
56
|
+
* reason on it.
|
|
57
|
+
*/
|
|
58
|
+
export async function describeAvpResources(options) {
|
|
59
|
+
const env = options.env ?? process.env;
|
|
60
|
+
const typesByName = {};
|
|
61
|
+
for (const name of options.entityNames) {
|
|
62
|
+
typesByName[name] = options.entities.get(name)?.entityType ?? CEDAR_POLICY_TYPE;
|
|
63
|
+
}
|
|
64
|
+
const policyStoreId = resolvePolicyStoreId({
|
|
65
|
+
environment: options.environment,
|
|
66
|
+
...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
|
|
67
|
+
entities: options.entities,
|
|
68
|
+
env,
|
|
69
|
+
});
|
|
70
|
+
if (!policyStoreId) {
|
|
71
|
+
return observation({}, unobservedAll(options.entityNames, "no-binding", `no AVP policy store for environment "${options.environment}" — set ${"CEDAR_AVP_POLICY_STORE_ID"} or declare policyStoreId on a policy`, typesByName));
|
|
72
|
+
}
|
|
73
|
+
if (!credentialsAvailable(env)) {
|
|
74
|
+
return observation({}, unobservedAll(options.entityNames, "no-credentials", "no AWS credentials and no endpoint override — the AVP store was not queried", typesByName));
|
|
75
|
+
}
|
|
76
|
+
const region = options.client?.region;
|
|
77
|
+
const declared = options.entityNames.map((name) => {
|
|
78
|
+
const entity = options.entities.get(name);
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
type: entity?.entityType ?? CEDAR_POLICY_TYPE,
|
|
82
|
+
props: entity?.props ?? {},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const adapter = {
|
|
86
|
+
async bind() {
|
|
87
|
+
try {
|
|
88
|
+
const policies = await loadLivePolicies({
|
|
89
|
+
policyStoreId,
|
|
90
|
+
...(options.client ? { client: options.client } : {}),
|
|
91
|
+
});
|
|
92
|
+
return { policyStoreId, byCedarId: indexByCedarId(policies), ...(region ? { region } : {}) };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// A store that is not there yet is the pre-first-apply state: nothing
|
|
96
|
+
// is deployed, so every declared policy is genuinely absent and
|
|
97
|
+
// `create` is the right proposal.
|
|
98
|
+
if (storeDoesNotExist(err)) {
|
|
99
|
+
return { policyStoreId, byCedarId: new Map(), ...(region ? { region } : {}) };
|
|
100
|
+
}
|
|
101
|
+
const { reason, detail } = classifyAvpFailure(err);
|
|
102
|
+
throw new AvpBindFailure(reason, `ListPolicies failed for store ${policyStoreId}: ${detail}`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
classifyBindFailure(err) {
|
|
106
|
+
if (err instanceof AvpBindFailure)
|
|
107
|
+
return { reason: err.reason, detail: err.detail };
|
|
108
|
+
const { reason, detail } = classifyAvpFailure(err);
|
|
109
|
+
return { reason, detail: `${storeAddress(policyStoreId, region)}: ${detail}` };
|
|
110
|
+
},
|
|
111
|
+
async read(binding, entity) {
|
|
112
|
+
if (entity.type !== CEDAR_POLICY_TYPE) {
|
|
113
|
+
// Cedar declares one deployable kind. Anything else is a schema entity
|
|
114
|
+
// or action constant, which has no AVP counterpart at all — saying so
|
|
115
|
+
// per entity keeps it out of `lifecycle plan` as a create.
|
|
116
|
+
return {
|
|
117
|
+
unobserved: {
|
|
118
|
+
reason: "unsupported-kind",
|
|
119
|
+
detail: `no AVP reader for ${entity.type} — only ${CEDAR_POLICY_TYPE} is deployed to a policy store`,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const cedarPolicyId = resolvePolicyId(entity.name, entity.props);
|
|
124
|
+
const queried = policyAddress(binding.policyStoreId, cedarPolicyId, binding.region);
|
|
125
|
+
const policy = binding.byCedarId.get(cedarPolicyId);
|
|
126
|
+
// The store was enumerated and this `@id` is not in it. That is an
|
|
127
|
+
// absence, and the only shape allowed to become a create.
|
|
128
|
+
if (!policy)
|
|
129
|
+
return { absent: true, queried };
|
|
130
|
+
const ownership = ownershipFromDescription(policy.description);
|
|
131
|
+
if (options.owned && ownership !== "owned") {
|
|
132
|
+
return {
|
|
133
|
+
unobserved: {
|
|
134
|
+
reason: "filtered",
|
|
135
|
+
detail: "live policy carries no chant description marker",
|
|
136
|
+
},
|
|
137
|
+
queried,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { present: metadataFor(policy, ownership), queried };
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return observeEntities(declared, adapter);
|
|
144
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed embedding of a cedar policy into `AWS::VerifiedPermissions::Policy`
|
|
3
|
+
* (#1652).
|
|
4
|
+
*
|
|
5
|
+
* The aws lexicon keeps the deployment vehicle — its generated
|
|
6
|
+
* `VerifiedPermissionsPolicy` takes a required `PolicyStoreId` and a required
|
|
7
|
+
* `Definition`, and the `Definition.Static.Statement` inside it is an opaque
|
|
8
|
+
* string. Opaque is the problem the whole cedar lexicon exists to remove: today
|
|
9
|
+
* a team writes that string with `${}` templating and finds out at deploy time
|
|
10
|
+
* whether the entity type exists.
|
|
11
|
+
*
|
|
12
|
+
* This module closes the loop without joining the two lexicons together. It
|
|
13
|
+
* emits the *string* and the *JSON envelope* AVP expects and stops there:
|
|
14
|
+
* nothing here imports `@intentius/chant-lexicon-aws`, and nothing here needs
|
|
15
|
+
* to. A project that has both installed writes
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* new VerifiedPermissionsPolicy({
|
|
19
|
+
* PolicyStoreId: store.ref(),
|
|
20
|
+
* Definition: avpPolicyDefinition("ownerRead", ownerReadProps),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* and the statement is the same text `chant build` writes to `.cedar`, rendered
|
|
25
|
+
* by the same renderer, with `@id` intact.
|
|
26
|
+
*
|
|
27
|
+
* ### Why no dependency, and why the example uses plain objects
|
|
28
|
+
*
|
|
29
|
+
* A cedar → aws dependency would invert the epic's decision that Cedar is
|
|
30
|
+
* vendor-neutral and AVP is one of several evaluators, and it would make the
|
|
31
|
+
* cedar lexicon unbuildable without the aws one. The seam is therefore the
|
|
32
|
+
* data shape, which is stable CloudFormation, and
|
|
33
|
+
* `examples/avp-embedding/` demonstrates the pairing with a plain-object stand-in
|
|
34
|
+
* for `VerifiedPermissionsPolicy` — the shipped examples build with the cedar
|
|
35
|
+
* serializer alone, so a real cross-lexicon example could not run in this
|
|
36
|
+
* repo's example harness at all. The stand-in carries the exact prop names the
|
|
37
|
+
* generated class declares (`PolicyStoreId`, `Definition`, `Name`), so the
|
|
38
|
+
* substitution is a one-line edit in a real project.
|
|
39
|
+
*/
|
|
40
|
+
import { cedarPolicyRecords, renderPolicyText, resolvePolicyId, } from "../serializer.js";
|
|
41
|
+
import { policyToJson } from "../spec/wasm.js";
|
|
42
|
+
import { encodeOwnershipDescription } from "./ownership.js";
|
|
43
|
+
function propsRecord(props) {
|
|
44
|
+
return props;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The Cedar policy text for one policy — the exact string
|
|
48
|
+
* `Definition.Static.Statement` wants.
|
|
49
|
+
*
|
|
50
|
+
* `name` is the chant entity name; the `@id` annotation the statement carries
|
|
51
|
+
* is derived from it the same way the serializer derives it, which is what lets
|
|
52
|
+
* the live observation match a policy in the store back to the entity that
|
|
53
|
+
* declared it.
|
|
54
|
+
*/
|
|
55
|
+
export function avpStatement(name, props, options = {}) {
|
|
56
|
+
const record = propsRecord(props);
|
|
57
|
+
return renderPolicyText(options.policyId ?? resolvePolicyId(name, record), record);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The Cedar JSON policy format for one policy — for an evaluator that takes
|
|
61
|
+
* JSON rather than text (`cedar-agent`, an embedded `cedar-wasm`).
|
|
62
|
+
*
|
|
63
|
+
* Not what AVP's `Definition` wants; AVP takes the text. Emitted here so the
|
|
64
|
+
* seam is documented in both directions rather than only the AWS one.
|
|
65
|
+
*
|
|
66
|
+
* Built the way the serializer's JSON leg is built (#1653): render the text,
|
|
67
|
+
* then hand it to `cedar-wasm`, so what comes back is Cedar's own reading
|
|
68
|
+
* rather than a second, worse encoder. Throws on text Cedar refuses — this is
|
|
69
|
+
* an authoring-time call, and a policy the module cannot read is a defect the
|
|
70
|
+
* caller wants to hear about rather than a `undefined` to thread through.
|
|
71
|
+
*/
|
|
72
|
+
export function avpStatementJSON(name, props, options = {}) {
|
|
73
|
+
const statement = avpStatement(name, props, options);
|
|
74
|
+
const converted = policyToJson(statement);
|
|
75
|
+
if (!converted.ok) {
|
|
76
|
+
throw new Error(`cedar: ${name} did not parse, so no JSON form could be produced — ${converted.error}`);
|
|
77
|
+
}
|
|
78
|
+
return converted.value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The `Definition` property of `AWS::VerifiedPermissions::Policy`.
|
|
82
|
+
*
|
|
83
|
+
* With `options.ownership` set, the description carries chant's marker — the
|
|
84
|
+
* only per-policy ownership channel AVP has, because policies are not taggable.
|
|
85
|
+
*/
|
|
86
|
+
export function avpPolicyDefinition(name, props, options = {}) {
|
|
87
|
+
const record = propsRecord(props);
|
|
88
|
+
const policyId = options.policyId ?? resolvePolicyId(name, record);
|
|
89
|
+
const description = options.ownership
|
|
90
|
+
? encodeOwnershipDescription(options.description, options.ownership, policyId)
|
|
91
|
+
: options.description?.trim();
|
|
92
|
+
return {
|
|
93
|
+
Static: {
|
|
94
|
+
Statement: renderPolicyText(policyId, record),
|
|
95
|
+
...(description ? { Description: description } : {}),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Both required props of `AWS::VerifiedPermissions::Policy`, ready to spread
|
|
101
|
+
* into the generated class.
|
|
102
|
+
*
|
|
103
|
+
* `PolicyStoreId` is a string here rather than an AttrRef because this module
|
|
104
|
+
* does not know the aws lexicon's reference types. A project passes
|
|
105
|
+
* `store.ref()` in place of the literal and TypeScript is satisfied by the
|
|
106
|
+
* generated class's own prop type, not by this one.
|
|
107
|
+
*/
|
|
108
|
+
export function avpPolicyResource(name, props, policyStoreId, options = {}) {
|
|
109
|
+
return { PolicyStoreId: policyStoreId, Definition: avpPolicyDefinition(name, props, options) };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Every `Cedar::Policy` in a build, rendered as AVP definitions and keyed by
|
|
113
|
+
* chant entity name.
|
|
114
|
+
*
|
|
115
|
+
* The whole-set form: one call turns a policy set into the definitions a stack
|
|
116
|
+
* of `VerifiedPermissionsPolicy` declarations needs, with references between
|
|
117
|
+
* declared entities already walked (that is what `cedarPolicyRecords` does).
|
|
118
|
+
*/
|
|
119
|
+
export function avpPolicySet(entities, options = {}) {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const { name, id, props } of cedarPolicyRecords(entities)) {
|
|
122
|
+
out[name] = avpPolicyDefinition(name, props, { ...options, policyId: id });
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|