@malloy-publisher/server 0.0.247 → 0.0.248
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/app/api-doc.yaml +15 -8
- package/dist/package_load_worker.mjs +4160 -2158
- package/dist/server.mjs +1069 -190
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -246982,11 +246982,67 @@ var init_package_load_pool = __esm(() => {
|
|
|
246982
246982
|
});
|
|
246983
246983
|
|
|
246984
246984
|
// src/service/authorize.ts
|
|
246985
|
+
import { payloadOf, routeOf } from "@malloydata/malloy";
|
|
246986
|
+
function noteRoute(text) {
|
|
246987
|
+
return routeOf({ value: text.trimStart() });
|
|
246988
|
+
}
|
|
246989
|
+
function notePayload(text) {
|
|
246990
|
+
return payloadOf({ value: text.trimStart() }) ?? "";
|
|
246991
|
+
}
|
|
246992
|
+
function authorizeNoteContent(text) {
|
|
246993
|
+
return noteRoute(text) === AUTHORIZE_ROUTE ? notePayload(text) : undefined;
|
|
246994
|
+
}
|
|
246985
246995
|
function assertNoCallerAuthorizeAnnotation(callerText) {
|
|
246986
246996
|
if (!AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText))
|
|
246987
246997
|
return;
|
|
246988
246998
|
throw new BadRequestError("An `authorize` annotation is not permitted in caller-submitted Malloy " + "text. Access gates are declared by the model author on the source; a " + "request cannot introduce, replace, or relax one. To validate a gate " + "you are authoring, save it to the package's model file and reload the " + "package — model load validates every `#(authorize)` annotation it " + "declares.");
|
|
246989
246999
|
}
|
|
247000
|
+
function containsAuthorizeAnnotationTag(texts) {
|
|
247001
|
+
return texts.some((text) => authorizeNoteContent(text) !== undefined);
|
|
247002
|
+
}
|
|
247003
|
+
function collectAuthorizeNearMisses(texts) {
|
|
247004
|
+
const found = [];
|
|
247005
|
+
for (const text of texts) {
|
|
247006
|
+
const trimmed2 = text.trimStart();
|
|
247007
|
+
const route = noteRoute(trimmed2);
|
|
247008
|
+
if (route === AUTHORIZE_ROUTE)
|
|
247009
|
+
continue;
|
|
247010
|
+
const nearMiss = route === undefined ? MALFORMED_AUTHORIZE_ATTEMPT.test(trimmed2) : route === "" ? MOTLY_AUTHORIZE_PAYLOAD.test(notePayload(trimmed2)) : route.toLowerCase() === AUTHORIZE_ROUTE;
|
|
247011
|
+
if (!nearMiss)
|
|
247012
|
+
continue;
|
|
247013
|
+
found.push(trimmed2.split(/[\r\n]/, 1)[0]);
|
|
247014
|
+
}
|
|
247015
|
+
return found;
|
|
247016
|
+
}
|
|
247017
|
+
function assertNoAuthorizeNearMisses(found) {
|
|
247018
|
+
if (found.length === 0)
|
|
247019
|
+
return;
|
|
247020
|
+
const unique = [...new Set(found)];
|
|
247021
|
+
throw new ModelCompilationError({
|
|
247022
|
+
message: `These annotations are not \`authorize\` gates and nothing enforces ` + `them:
|
|
247023
|
+
${unique.map((t) => ` - \`${t}\``).join(`
|
|
247024
|
+
`)}
|
|
247025
|
+
` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` (or \`##(authorize)\`, or the block form ` + `\`#|(authorize)\`) reaches the authorize route — a space after the ` + `\`#\`, spaces inside the brackets, or anything trailing the closing ` + `bracket makes it a plain tag Malloy hands to something else. Write ` + `\`#(authorize) "<expression>"\` on the \`source:\` statement you mean ` + `to protect. This is refused rather than interpreted: guessing at the ` + `intent would let publisher start enforcing a filter on a package that ` + `has been serving every row.`
|
|
247026
|
+
});
|
|
247027
|
+
}
|
|
247028
|
+
function describeMisplacedAuthorizeAnnotation(f) {
|
|
247029
|
+
if (f.kind === "query")
|
|
247030
|
+
return `on query "${f.name}"`;
|
|
247031
|
+
if (f.kind === "file")
|
|
247032
|
+
return "at the file level (`##(authorize)`)";
|
|
247033
|
+
return `on field "${f.fieldName}" of source "${f.name}"`;
|
|
247034
|
+
}
|
|
247035
|
+
function assertNoMisplacedAuthorizeAnnotations(found) {
|
|
247036
|
+
if (found.length === 0)
|
|
247037
|
+
return;
|
|
247038
|
+
const positions = found.map((f) => ` - ${describeMisplacedAuthorizeAnnotation(f)}`).join(`
|
|
247039
|
+
`);
|
|
247040
|
+
throw new ModelCompilationError({
|
|
247041
|
+
message: `An \`#(authorize)\` annotation is never enforced at:
|
|
247042
|
+
${positions}
|
|
247043
|
+
` + `A gate only applies where model load looks for one — a \`source:\`'s ` + `own annotation, or one it inherits from an \`extend\`/query-source ` + `base. File-level \`##(authorize)\` is deprecated and no longer ` + `enforced anywhere, so it always lands here: declare \`#(authorize)\` ` + `on each \`source:\` it was meant to protect instead. Every other ` + `position above should move to the \`source:\` statement it is meant ` + `to protect.`
|
|
247044
|
+
});
|
|
247045
|
+
}
|
|
246990
247046
|
function buildAuthorizeProbe(exprs, givenDecls = []) {
|
|
246991
247047
|
const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
|
|
246992
247048
|
`);
|
|
@@ -247014,6 +247070,158 @@ function referencedGivenNames(expr) {
|
|
|
247014
247070
|
}
|
|
247015
247071
|
return names;
|
|
247016
247072
|
}
|
|
247073
|
+
function classifyAuthorizeGate(condition, declaredTypes, declaredDefaults) {
|
|
247074
|
+
const fieldUsage = condition.refSummary?.fieldUsage;
|
|
247075
|
+
if (!Array.isArray(fieldUsage) || fieldUsage.length === 0) {
|
|
247076
|
+
return { shape: "given_only" };
|
|
247077
|
+
}
|
|
247078
|
+
const givenNames = [];
|
|
247079
|
+
const literalAtoms = [];
|
|
247080
|
+
let rejection;
|
|
247081
|
+
const reject = (cause, detail) => {
|
|
247082
|
+
rejection ??= { shape: "rejected", cause, detail };
|
|
247083
|
+
return false;
|
|
247084
|
+
};
|
|
247085
|
+
const givenOperand = (node) => {
|
|
247086
|
+
const n = asNode(node);
|
|
247087
|
+
return n && n.node === "given" && typeof n.refName === "string" ? n.refName : null;
|
|
247088
|
+
};
|
|
247089
|
+
const isLiteralOperand = (node) => {
|
|
247090
|
+
const n = asNode(node);
|
|
247091
|
+
return !!n && (n.node === "numberLiteral" || n.node === "stringLiteral" || n.node === "true" || n.node === "false");
|
|
247092
|
+
};
|
|
247093
|
+
const literalOperandText = (node) => {
|
|
247094
|
+
const n = asNode(node);
|
|
247095
|
+
if (!n)
|
|
247096
|
+
return null;
|
|
247097
|
+
if (n.node === "true" || n.node === "false")
|
|
247098
|
+
return n.node;
|
|
247099
|
+
if (n.node === "numberLiteral" && typeof n.literal === "string") {
|
|
247100
|
+
return n.literal;
|
|
247101
|
+
}
|
|
247102
|
+
if (n.node === "stringLiteral" && typeof n.literal === "string") {
|
|
247103
|
+
return `'${n.literal.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
247104
|
+
}
|
|
247105
|
+
return null;
|
|
247106
|
+
};
|
|
247107
|
+
const declaredTypeOf = (name) => {
|
|
247108
|
+
const declared = declaredTypes.get(name);
|
|
247109
|
+
if (declared === undefined) {
|
|
247110
|
+
reject("unreachable_given", `\`$${name}\` is not on this model's given surface, so the gate ` + `would bind its declaration default rather than the caller's ` + `value. Declare it here, importing it if it lives elsewhere ` + `(\`import { ${name} } from "…"\`)`);
|
|
247111
|
+
return null;
|
|
247112
|
+
}
|
|
247113
|
+
return declared;
|
|
247114
|
+
};
|
|
247115
|
+
const walk = (node, depth) => {
|
|
247116
|
+
if (depth > MAX_GATE_WALK_DEPTH) {
|
|
247117
|
+
return reject("unsupported_node", "the gate nests too deeply to read");
|
|
247118
|
+
}
|
|
247119
|
+
const n = asNode(node);
|
|
247120
|
+
if (!n || typeof n.node !== "string") {
|
|
247121
|
+
return reject("unsupported_node", "the gate has an unreadable shape");
|
|
247122
|
+
}
|
|
247123
|
+
const kind = n.node;
|
|
247124
|
+
if (BOOLEAN_NODES.has(kind)) {
|
|
247125
|
+
const kids = asNode(n.kids);
|
|
247126
|
+
if (!kids) {
|
|
247127
|
+
return reject("unsupported_node", `\`${kind}\` has no operands`);
|
|
247128
|
+
}
|
|
247129
|
+
return walk(kids.left, depth + 1) && walk(kids.right, depth + 1);
|
|
247130
|
+
}
|
|
247131
|
+
if (TRANSPARENT_NODES.has(kind)) {
|
|
247132
|
+
return walk(n.e, depth + 1);
|
|
247133
|
+
}
|
|
247134
|
+
if (kind === "inGiven") {
|
|
247135
|
+
if (n.not === true) {
|
|
247136
|
+
return reject("unsupported_node", "a negated membership test (`not in`) is not an access rule; " + "write the gate as the set of rows a caller MAY read");
|
|
247137
|
+
}
|
|
247138
|
+
const given = givenOperand(n.givenRef);
|
|
247139
|
+
if (given === null) {
|
|
247140
|
+
return reject("unsupported_node", "`in` must test membership of a declared given");
|
|
247141
|
+
}
|
|
247142
|
+
const declared = declaredTypeOf(given);
|
|
247143
|
+
if (declared === null)
|
|
247144
|
+
return false;
|
|
247145
|
+
if (!isArrayType(declared)) {
|
|
247146
|
+
return reject("scalar_given_rejects_in", `\`$${given}\` is declared \`${declared}\` (a scalar), so ` + `\`in $${given}\` is not a membership test. Compare it with ` + `\`=\` instead`);
|
|
247147
|
+
}
|
|
247148
|
+
givenNames.push(given);
|
|
247149
|
+
return walkFieldOperand(n.e);
|
|
247150
|
+
}
|
|
247151
|
+
if (SCALAR_COMPARISON_NODES.has(kind)) {
|
|
247152
|
+
const kids = asNode(n.kids);
|
|
247153
|
+
if (!kids) {
|
|
247154
|
+
return reject("unsupported_node", `\`${kind}\` has no operands`);
|
|
247155
|
+
}
|
|
247156
|
+
const left = givenOperand(kids.left);
|
|
247157
|
+
const right = givenOperand(kids.right);
|
|
247158
|
+
const given = left ?? right;
|
|
247159
|
+
if (given === null) {
|
|
247160
|
+
return reject("no_given_reference", `\`${kind}\` must compare a field against a given; a comparison ` + `against a constant is a fixed filter and belongs in the ` + `source's own \`where:\``);
|
|
247161
|
+
}
|
|
247162
|
+
const declared = declaredTypeOf(given);
|
|
247163
|
+
if (declared === null)
|
|
247164
|
+
return false;
|
|
247165
|
+
if (isArrayType(declared)) {
|
|
247166
|
+
return reject("array_given_needs_in", `\`$${given}\` is declared \`${declared}\` (an array), so ` + `comparing it with \`${kind}\` compiles and then fails in the ` + `warehouse. Write \`in $${given}\` — it is also the spelling ` + `that matches no rows when the array is empty`);
|
|
247167
|
+
}
|
|
247168
|
+
const otherSide = left === null ? kids.left : kids.right;
|
|
247169
|
+
if (isLiteralOperand(otherSide)) {
|
|
247170
|
+
givenNames.push(given);
|
|
247171
|
+
const literalText = literalOperandText(otherSide);
|
|
247172
|
+
if (literalText !== null) {
|
|
247173
|
+
literalAtoms.push(left !== null ? `$${given} ${kind} ${literalText}` : `${literalText} ${kind} $${given}`);
|
|
247174
|
+
}
|
|
247175
|
+
return true;
|
|
247176
|
+
}
|
|
247177
|
+
if (declaredDefaults.has(given)) {
|
|
247178
|
+
return reject("field_given_has_default", `\`${kind}\` compares row field data against \`$${given}\`, ` + `which is declared with a default (\`${declaredDefaults.get(given)}\`). ` + `A caller who supplies no value for \`$${given}\` gets that ` + `default, and the comparison then applies to every row — ` + `admitting rows it was meant to exclude. Declare \`$${given}\` ` + `with no default so a caller must supply one explicitly`);
|
|
247179
|
+
}
|
|
247180
|
+
givenNames.push(given);
|
|
247181
|
+
return walkFieldOperand(otherSide);
|
|
247182
|
+
}
|
|
247183
|
+
return reject("unsupported_node", `\`${kind}\` is not permitted in a gate; a row-level gate is a ` + `boolean combination of \`<field> <operator> $GIVEN\` comparisons`);
|
|
247184
|
+
};
|
|
247185
|
+
const walkFieldOperand = (node) => {
|
|
247186
|
+
const n = asNode(node);
|
|
247187
|
+
if (!n || n.node !== "field" || !Array.isArray(n.path)) {
|
|
247188
|
+
return reject("unsupported_node", `a gate compares a FIELD against a given; \`${asNode(node)?.node ?? "that operand"}\` is not a field reference`);
|
|
247189
|
+
}
|
|
247190
|
+
return true;
|
|
247191
|
+
};
|
|
247192
|
+
let ok;
|
|
247193
|
+
try {
|
|
247194
|
+
ok = walk(condition.e, 0);
|
|
247195
|
+
} catch {
|
|
247196
|
+
return {
|
|
247197
|
+
shape: "rejected",
|
|
247198
|
+
cause: "unsupported_node",
|
|
247199
|
+
detail: "the gate's compiled shape could not be read"
|
|
247200
|
+
};
|
|
247201
|
+
}
|
|
247202
|
+
if (!ok) {
|
|
247203
|
+
return rejection ?? {
|
|
247204
|
+
shape: "rejected",
|
|
247205
|
+
cause: "unsupported_node",
|
|
247206
|
+
detail: "the gate is not an allowed shape"
|
|
247207
|
+
};
|
|
247208
|
+
}
|
|
247209
|
+
if (givenNames.length === 0) {
|
|
247210
|
+
return {
|
|
247211
|
+
shape: "rejected",
|
|
247212
|
+
cause: "no_given_reference",
|
|
247213
|
+
detail: "a row-level gate must compare a field against a given; a gate " + "that references none is a fixed filter and belongs in the " + "source's own `where:`"
|
|
247214
|
+
};
|
|
247215
|
+
}
|
|
247216
|
+
return { shape: "row_level", givenNames, literalAtoms };
|
|
247217
|
+
}
|
|
247218
|
+
function isArrayType(declaredType) {
|
|
247219
|
+
const normalized = declaredType.trim().toLowerCase();
|
|
247220
|
+
return normalized === "array" || normalized.endsWith("[]");
|
|
247221
|
+
}
|
|
247222
|
+
function asNode(node) {
|
|
247223
|
+
return node !== null && typeof node === "object" ? node : null;
|
|
247224
|
+
}
|
|
247017
247225
|
function inferGivenType(value) {
|
|
247018
247226
|
if (typeof value === "string")
|
|
247019
247227
|
return "string";
|
|
@@ -247056,9 +247264,8 @@ async function runProbe(executor, probeText, givens) {
|
|
|
247056
247264
|
}
|
|
247057
247265
|
async function evaluateAuthorize(executor, exprs, givens, declaredTypes, options) {
|
|
247058
247266
|
const selfContainedFirst = options?.selfContainedFirst ?? false;
|
|
247059
|
-
const
|
|
247060
|
-
|
|
247061
|
-
if (selfContainedFirst && index >= ambientPrefix) {
|
|
247267
|
+
for (const expr of exprs) {
|
|
247268
|
+
if (selfContainedFirst) {
|
|
247062
247269
|
if (await evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)) {
|
|
247063
247270
|
return true;
|
|
247064
247271
|
}
|
|
@@ -247106,27 +247313,123 @@ async function evaluateSelfContainedFirst(executor, expr, givens, declaredTypes)
|
|
|
247106
247313
|
}
|
|
247107
247314
|
}
|
|
247108
247315
|
}
|
|
247109
|
-
|
|
247110
|
-
|
|
247111
|
-
|
|
247112
|
-
|
|
247113
|
-
|
|
247316
|
+
function quoteMalloyIdentifier(name) {
|
|
247317
|
+
return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
|
|
247318
|
+
}
|
|
247319
|
+
function buildRowLevelProbe(graftTarget, filterText) {
|
|
247320
|
+
return `run: ${quoteMalloyIdentifier(graftTarget)} extend { where: ${filterText} } -> { select: __authorize_probe is 1; limit: 1 }`;
|
|
247321
|
+
}
|
|
247322
|
+
function liftProbeFilterCondition(prepared, label, filterText) {
|
|
247323
|
+
const filterList = prepared._query?.structRef?.filterList;
|
|
247324
|
+
if (!Array.isArray(filterList) || filterList.length === 0) {
|
|
247325
|
+
throw new Error(`${label} carries no filter condition`);
|
|
247326
|
+
}
|
|
247327
|
+
const lifted = filterList[filterList.length - 1];
|
|
247328
|
+
if (lifted.code !== filterText) {
|
|
247329
|
+
throw new Error(`${label} carries the wrong condition — expected "${filterText}", got "${lifted.code ?? ""}"`);
|
|
247330
|
+
}
|
|
247331
|
+
if (!lifted.isSourceFilter) {
|
|
247332
|
+
throw new Error(`${label} carries a condition that is not a source filter`);
|
|
247333
|
+
}
|
|
247334
|
+
return lifted;
|
|
247335
|
+
}
|
|
247336
|
+
function gateFilterText(exprs) {
|
|
247337
|
+
return exprs.map((e) => `(${e})`).join(" or ");
|
|
247338
|
+
}
|
|
247339
|
+
async function liftRowLevelCondition(compiler, sourceName, exprs) {
|
|
247340
|
+
const filterText = gateFilterText(exprs);
|
|
247341
|
+
const prepared = await compiler.loadQuery(buildRowLevelProbe(sourceName, filterText)).getPreparedQuery();
|
|
247342
|
+
return liftProbeFilterCondition(prepared, `row-level probe for "${sourceName}"`, filterText);
|
|
247343
|
+
}
|
|
247344
|
+
async function runOneRowProbeOrThrow(compiler, sourceName, exprs) {
|
|
247345
|
+
try {
|
|
247346
|
+
await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
|
|
247347
|
+
} catch (err) {
|
|
247348
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
247349
|
+
throw new ModelCompilationError({
|
|
247350
|
+
message: `Invalid #(authorize) annotation on source "${sourceName}" [${exprs.join(" | ")}]: ${detail}`
|
|
247351
|
+
});
|
|
247352
|
+
}
|
|
247353
|
+
}
|
|
247354
|
+
async function assertNoVacuousDefaultAtom(executor, sourceName, literalAtoms) {
|
|
247355
|
+
for (const atom of literalAtoms) {
|
|
247356
|
+
let vacuous;
|
|
247114
247357
|
try {
|
|
247115
|
-
await
|
|
247358
|
+
vacuous = await runProbe(executor, buildAuthorizeProbe([atom]), {});
|
|
247116
247359
|
} catch (err) {
|
|
247117
247360
|
const detail = err instanceof Error ? err.message : String(err);
|
|
247361
|
+
if (NO_DEFAULT_GIVEN_PATTERN.test(detail))
|
|
247362
|
+
continue;
|
|
247118
247363
|
throw new ModelCompilationError({
|
|
247119
|
-
message: `Invalid #(authorize) annotation on source "${
|
|
247364
|
+
message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` could not be evaluated against its ` + `given's declared default (${detail}).`
|
|
247120
247365
|
});
|
|
247121
247366
|
}
|
|
247367
|
+
if (vacuous) {
|
|
247368
|
+
throw new ModelCompilationError({
|
|
247369
|
+
message: `Invalid #(authorize) annotation on source "${sourceName}": ` + `the atom \`${atom}\` evaluates to TRUE when a caller supplies ` + `no givens (its given's own declared default). An OR'd atom ` + `that is true by default makes the whole row filter admit ` + `every row for that caller. Give the given a default this ` + `atom evaluates false against, or declare it with no default ` + `so a caller must supply one explicitly.`
|
|
247370
|
+
});
|
|
247371
|
+
}
|
|
247372
|
+
}
|
|
247373
|
+
}
|
|
247374
|
+
async function validateAuthorizeProbes(compiler, options) {
|
|
247375
|
+
const declaredTypes = options.declaredTypes ?? new Map;
|
|
247376
|
+
const declaredDefaults = options.declaredDefaults ?? new Map;
|
|
247377
|
+
const provenNoteObjects = new Set;
|
|
247378
|
+
const ownNotesOf = options.authorizeOwnNotes ?? new Map;
|
|
247379
|
+
const pending = [];
|
|
247380
|
+
for (const [sourceName, groups] of options.authorizeMap ?? []) {
|
|
247381
|
+
for (const exprs of groups) {
|
|
247382
|
+
if (exprs.length === 0)
|
|
247383
|
+
continue;
|
|
247384
|
+
let condition;
|
|
247385
|
+
try {
|
|
247386
|
+
condition = await liftRowLevelCondition(compiler, sourceName, exprs);
|
|
247387
|
+
} catch (err) {
|
|
247388
|
+
pending.push({ sourceName, exprs, err });
|
|
247389
|
+
continue;
|
|
247390
|
+
}
|
|
247391
|
+
const classification = classifyAuthorizeGate(condition, declaredTypes, declaredDefaults);
|
|
247392
|
+
if (classification.shape === "given_only") {
|
|
247393
|
+
await runOneRowProbeOrThrow(compiler, sourceName, exprs);
|
|
247394
|
+
for (const note of ownNotesOf.get(sourceName) ?? []) {
|
|
247395
|
+
provenNoteObjects.add(note);
|
|
247396
|
+
}
|
|
247397
|
+
continue;
|
|
247398
|
+
}
|
|
247399
|
+
if (classification.shape === "rejected") {
|
|
247400
|
+
options.onRowLevelGateRejected?.(classification.cause);
|
|
247401
|
+
const ownNotes = ownNotesOf.get(sourceName) ?? [];
|
|
247402
|
+
if (ownNotes.length === 0) {
|
|
247403
|
+
options.onRowLevelGateUnexpressible?.(sourceName, classification.detail);
|
|
247404
|
+
continue;
|
|
247405
|
+
}
|
|
247406
|
+
throw new ModelCompilationError({
|
|
247407
|
+
message: `Invalid #(authorize) annotation on source "${sourceName}" ` + `[${exprs.join(" | ")}]: ${classification.detail}`
|
|
247408
|
+
});
|
|
247409
|
+
}
|
|
247410
|
+
await assertNoVacuousDefaultAtom(compiler, sourceName, classification.literalAtoms);
|
|
247411
|
+
for (const note of ownNotesOf.get(sourceName) ?? []) {
|
|
247412
|
+
provenNoteObjects.add(note);
|
|
247413
|
+
}
|
|
247414
|
+
}
|
|
247415
|
+
}
|
|
247416
|
+
for (const failure of pending) {
|
|
247417
|
+
const ownNotes = ownNotesOf.get(failure.sourceName) ?? [];
|
|
247418
|
+
const inherited = ownNotes.length === 0 || ownNotes.every((note) => provenNoteObjects.has(note));
|
|
247419
|
+
if (inherited) {
|
|
247420
|
+
const detail = failure.err instanceof Error ? failure.err.message : String(failure.err);
|
|
247421
|
+
options.onRowLevelGateRejected?.("entry_point_unexpressible");
|
|
247422
|
+
options.onRowLevelGateUnexpressible?.(failure.sourceName, detail);
|
|
247423
|
+
continue;
|
|
247424
|
+
}
|
|
247425
|
+
await runOneRowProbeOrThrow(compiler, failure.sourceName, failure.exprs);
|
|
247122
247426
|
}
|
|
247123
247427
|
}
|
|
247124
247428
|
function parseAuthorizeAnnotation(annotation) {
|
|
247125
|
-
const
|
|
247126
|
-
|
|
247127
|
-
if (!prefix)
|
|
247429
|
+
const content = authorizeNoteContent(annotation);
|
|
247430
|
+
if (content === undefined)
|
|
247128
247431
|
return null;
|
|
247129
|
-
return unwrapQuotedExpression(
|
|
247432
|
+
return unwrapQuotedExpression(content.trim());
|
|
247130
247433
|
}
|
|
247131
247434
|
function collectAuthorizeExprs(annotations) {
|
|
247132
247435
|
const exprs = [];
|
|
@@ -247174,14 +247477,20 @@ function unwrapQuotedExpression(body) {
|
|
|
247174
247477
|
}
|
|
247175
247478
|
return expr;
|
|
247176
247479
|
}
|
|
247177
|
-
var
|
|
247480
|
+
var AUTHORIZE_ROUTE = "authorize", AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, MOTLY_AUTHORIZE_PAYLOAD, MALFORMED_AUTHORIZE_ATTEMPT, GIVEN_REF_PATTERN, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, BOOLEAN_NODES, TRANSPARENT_NODES, SCALAR_COMPARISON_NODES, MAX_GATE_WALK_DEPTH = 64, NO_DEFAULT_GIVEN_PATTERN;
|
|
247178
247481
|
var init_authorize = __esm(() => {
|
|
247179
247482
|
init_errors();
|
|
247180
|
-
|
|
247181
|
-
AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(
|
|
247182
|
-
|
|
247483
|
+
AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*authorize(?=[)\]}>]|[ \t]|$)`;
|
|
247484
|
+
AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(AUTHORIZE_TAG_LIKE, "iu");
|
|
247485
|
+
MOTLY_AUTHORIZE_PAYLOAD = /^[ \t]*[([{<][ \t]*authorize[ \t]*[)\]}>]/iu;
|
|
247486
|
+
MALFORMED_AUTHORIZE_ATTEMPT = /^##?\|?[ \t]*[([{<]?[ \t]*authorize/iu;
|
|
247183
247487
|
GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
247184
247488
|
STRING_LITERAL_PATTERN = /'(?:\\.|[^'\\])*'/g;
|
|
247489
|
+
ROW_LEVEL_GATE_REJECTION_CAUSES = everyMemberOf()("array_given_needs_in", "scalar_given_rejects_in", "field_given_has_default", "unsupported_node", "no_given_reference", "unreachable_given", "entry_point_unexpressible");
|
|
247490
|
+
BOOLEAN_NODES = new Set(["and", "or"]);
|
|
247491
|
+
TRANSPARENT_NODES = new Set(["()"]);
|
|
247492
|
+
SCALAR_COMPARISON_NODES = new Set(["=", "!=", ">", ">=", "<", "<="]);
|
|
247493
|
+
NO_DEFAULT_GIVEN_PATTERN = /has no value and no default/;
|
|
247185
247494
|
});
|
|
247186
247495
|
|
|
247187
247496
|
// src/authorize_metrics.ts
|
|
@@ -247197,9 +247506,22 @@ function recordAuthorizeBypass(entryPoint) {
|
|
|
247197
247506
|
});
|
|
247198
247507
|
bypassCounter.add(1, { entry_point: entryPoint });
|
|
247199
247508
|
}
|
|
247200
|
-
|
|
247509
|
+
function recordRowLevelGateDecision(decision) {
|
|
247510
|
+
rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
|
|
247511
|
+
description: "How a row-level `#(authorize)` gate resolved a request. Label: decision ('denied_by_gate'|'empty_after_filter'). 'denied_by_gate' is the fail-closed refusal when the gate could not be applied; 'empty_after_filter' is a successful response with zero rows after the filter matched none, which is NOT an error."
|
|
247512
|
+
});
|
|
247513
|
+
rowLevelDecisionCounter.add(1, { decision });
|
|
247514
|
+
}
|
|
247515
|
+
function recordRowLevelGateRejected(cause) {
|
|
247516
|
+
rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
|
|
247517
|
+
description: "Row-level `#(authorize)` gates refused at package load because their compiled condition is not an allowed shape, or an inherited gate that could not be expressed at one derived entry point. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). All but 'entry_point_unexpressible' fail the whole model load; that one fires at load without failing it — see the doc above. Alert on any nonzero value since the last publish, not on a rate."
|
|
247518
|
+
});
|
|
247519
|
+
rowLevelRejectionCounter.add(1, { cause });
|
|
247520
|
+
}
|
|
247521
|
+
var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null;
|
|
247201
247522
|
var init_authorize_metrics = __esm(() => {
|
|
247202
247523
|
init_telemetry();
|
|
247524
|
+
init_authorize();
|
|
247203
247525
|
});
|
|
247204
247526
|
|
|
247205
247527
|
// src/materialization_metrics.ts
|
|
@@ -247800,7 +248122,10 @@ function annotationTexts(annote) {
|
|
|
247800
248122
|
return texts.length > 0 ? texts : undefined;
|
|
247801
248123
|
}
|
|
247802
248124
|
function ownLevelNoteTexts(annote) {
|
|
247803
|
-
return
|
|
248125
|
+
return ownLevelNotes(annote).map((note) => note.text);
|
|
248126
|
+
}
|
|
248127
|
+
function ownLevelNotes(annote) {
|
|
248128
|
+
return [...annote?.blockNotes ?? [], ...annote?.notes ?? []];
|
|
247804
248129
|
}
|
|
247805
248130
|
var init_annotations = () => {};
|
|
247806
248131
|
|
|
@@ -247995,6 +248320,18 @@ function assertMaterializationEligible(persistSource) {
|
|
|
247995
248320
|
});
|
|
247996
248321
|
}
|
|
247997
248322
|
}
|
|
248323
|
+
function assertColocatedPersistNotAuthorizeGated(persistSource, sourceName = persistSource.name, origin2 = "persist") {
|
|
248324
|
+
if (referencesAuthorize(persistSource)) {
|
|
248325
|
+
recordEligibilityRefused("authorize");
|
|
248326
|
+
const gated = origin2 === "preaggregate" ? `the source '${sourceName}' rolls up is protected by an ` + `#(authorize) gate (its own or a joined source's)` : `it is protected by an #(authorize) gate (its own or a joined ` + `source's)`;
|
|
248327
|
+
const remedy = origin2 === "preaggregate" ? `Remove the '#@ preaggregate' annotation from the gated source's ` + `measure(s), or move the gate to a source that is not ` + `pre-aggregated.` : `Drop '#@ persist' from this source, or move the gate to a source ` + `that is not materialized.`;
|
|
248328
|
+
const what = origin2 === "preaggregate" ? `Pre-aggregation rollup '${sourceName}' cannot be built` : `Source '${sourceName}' cannot be materialized (colocated ` + `'#@ persist')`;
|
|
248329
|
+
const alsoRollup = origin2 === "preaggregate" ? ` A rollup also groups ACROSS the gated column, so it could not ` + `be row-filtered afterwards even in principle.` : "";
|
|
248330
|
+
throw new MaterializationEligibilityError({
|
|
248331
|
+
message: `${what}: ${gated}. An authorize expression is evaluated per ` + `request; a materialized-once table served frozen carries no gate, ` + `so it would be served to everyone, bypassing authorization.` + `${alsoRollup} This is refused for safety. ${remedy}`
|
|
248332
|
+
});
|
|
248333
|
+
}
|
|
248334
|
+
}
|
|
247998
248335
|
function unboundParameterNames(persistSource) {
|
|
247999
248336
|
const def = persistSource._sourceDef;
|
|
248000
248337
|
if (def === null || typeof def !== "object") {
|
|
@@ -249248,16 +249585,182 @@ var init_preaggregation_validation = __esm(() => {
|
|
|
249248
249585
|
init_preaggregation_classifier();
|
|
249249
249586
|
});
|
|
249250
249587
|
|
|
249588
|
+
// src/service/gate_registry_walk.ts
|
|
249589
|
+
import { isSourceDef } from "@malloydata/malloy";
|
|
249590
|
+
function resolveDeclaredSource(struct, modelDef) {
|
|
249591
|
+
if (!modelDef)
|
|
249592
|
+
return { kind: "none" };
|
|
249593
|
+
let sawBrokenEntry = false;
|
|
249594
|
+
for (const id of [struct.referenceID, struct.sourceID]) {
|
|
249595
|
+
const entry = id ? modelDef.sourceRegistry?.[id]?.entry : undefined;
|
|
249596
|
+
if (!entry)
|
|
249597
|
+
continue;
|
|
249598
|
+
const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
|
|
249599
|
+
if (declared === struct)
|
|
249600
|
+
continue;
|
|
249601
|
+
if (!declared || !isSourceDef(declared)) {
|
|
249602
|
+
sawBrokenEntry = true;
|
|
249603
|
+
continue;
|
|
249604
|
+
}
|
|
249605
|
+
return { kind: "resolved", source: declared };
|
|
249606
|
+
}
|
|
249607
|
+
return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
|
|
249608
|
+
}
|
|
249609
|
+
function ancestorGateExprs(struct, modelDef, seen = new Set) {
|
|
249610
|
+
let inherited = struct.annotations?.inherits;
|
|
249611
|
+
for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
|
|
249612
|
+
const exprs2 = collectAuthorizeExprs(ownLevelNoteTexts(inherited));
|
|
249613
|
+
if (exprs2.length > 0)
|
|
249614
|
+
return exprs2;
|
|
249615
|
+
inherited = inherited.inherits;
|
|
249616
|
+
}
|
|
249617
|
+
if (inherited)
|
|
249618
|
+
return ["false"];
|
|
249619
|
+
seen.add(struct);
|
|
249620
|
+
if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
|
|
249621
|
+
return ["false"];
|
|
249622
|
+
const declared = resolveDeclaredSource(struct, modelDef);
|
|
249623
|
+
if (declared.kind === "unresolvable")
|
|
249624
|
+
return ["false"];
|
|
249625
|
+
if (declared.kind === "none" || seen.has(declared.source))
|
|
249626
|
+
return [];
|
|
249627
|
+
const exprs = collectAuthorizeExprs(ownLevelNoteTexts(declared.source.annotations));
|
|
249628
|
+
return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, seen);
|
|
249629
|
+
}
|
|
249630
|
+
function resolveQuerySourceBase(struct, modelDef) {
|
|
249631
|
+
const duck = struct;
|
|
249632
|
+
if (duck.type !== "query_source")
|
|
249633
|
+
return;
|
|
249634
|
+
const ref = duck.query?.structRef;
|
|
249635
|
+
const base = typeof ref === "string" ? modelDef?.contents[ref] : ref;
|
|
249636
|
+
return base && isSourceDef(base) ? base : undefined;
|
|
249637
|
+
}
|
|
249638
|
+
function resolveCompositeResolvedBase(struct) {
|
|
249639
|
+
const duck = struct;
|
|
249640
|
+
return duck.type === "query_source" ? duck.query?.compositeResolvedSourceDef : undefined;
|
|
249641
|
+
}
|
|
249642
|
+
function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
|
|
249643
|
+
const direct = ancestorGateExprs(struct, modelDef, new Set(seen));
|
|
249644
|
+
if (direct.length > 0)
|
|
249645
|
+
return [direct];
|
|
249646
|
+
if (seen.has(struct))
|
|
249647
|
+
return [];
|
|
249648
|
+
seen.add(struct);
|
|
249649
|
+
const groups = [];
|
|
249650
|
+
const base = resolveQuerySourceBase(struct, modelDef);
|
|
249651
|
+
if (!base) {
|
|
249652
|
+
const duck = struct;
|
|
249653
|
+
if (duck.type === "query_source")
|
|
249654
|
+
groups.push(["false"]);
|
|
249655
|
+
} else if (!seen.has(base)) {
|
|
249656
|
+
const ownExprs = collectAuthorizeExprs(ownLevelNoteTexts(base.annotations));
|
|
249657
|
+
groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, seen));
|
|
249658
|
+
}
|
|
249659
|
+
const composite = resolveCompositeResolvedBase(struct);
|
|
249660
|
+
if (composite && !seen.has(composite)) {
|
|
249661
|
+
const parentOwnNotes = base ? ownLevelNotes(base.annotations) : [];
|
|
249662
|
+
const compositeOwnNotes = ownLevelNotes(composite.annotations).filter((note) => !parentOwnNotes.includes(note));
|
|
249663
|
+
const compositeOwn = collectAuthorizeExprs(compositeOwnNotes.map((note) => note.text));
|
|
249664
|
+
groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, seen));
|
|
249665
|
+
}
|
|
249666
|
+
return groups;
|
|
249667
|
+
}
|
|
249668
|
+
function derivedStructsReachable(roots, modelDef) {
|
|
249669
|
+
const seen = new Set(roots);
|
|
249670
|
+
const found = [];
|
|
249671
|
+
const worklist = [...roots];
|
|
249672
|
+
for (let i = 0;i < worklist.length; i++) {
|
|
249673
|
+
const struct = worklist[i];
|
|
249674
|
+
for (const next of [
|
|
249675
|
+
resolveQuerySourceBase(struct, modelDef),
|
|
249676
|
+
resolveCompositeResolvedBase(struct)
|
|
249677
|
+
]) {
|
|
249678
|
+
if (!next || seen.has(next))
|
|
249679
|
+
continue;
|
|
249680
|
+
seen.add(next);
|
|
249681
|
+
found.push(next);
|
|
249682
|
+
worklist.push(next);
|
|
249683
|
+
}
|
|
249684
|
+
}
|
|
249685
|
+
return found;
|
|
249686
|
+
}
|
|
249687
|
+
var ANCESTOR_WALK_MAX_DEPTH = 32;
|
|
249688
|
+
var init_gate_registry_walk = __esm(() => {
|
|
249689
|
+
init_annotations();
|
|
249690
|
+
init_authorize();
|
|
249691
|
+
});
|
|
249692
|
+
|
|
249251
249693
|
// src/service/source_extraction.ts
|
|
249252
249694
|
import {
|
|
249253
|
-
|
|
249695
|
+
isJoined as isJoined2,
|
|
249696
|
+
isSourceDef as isSourceDef2
|
|
249254
249697
|
} from "@malloydata/malloy";
|
|
249698
|
+
function joinFieldNamesUnresolvableDeclaration(field, modelDef) {
|
|
249699
|
+
const ids = [field.referenceID, field.sourceID].filter((id) => !!id);
|
|
249700
|
+
if (ids.length === 0)
|
|
249701
|
+
return false;
|
|
249702
|
+
for (const id of ids) {
|
|
249703
|
+
const entry = modelDef.sourceRegistry?.[id]?.entry;
|
|
249704
|
+
if (!entry)
|
|
249705
|
+
continue;
|
|
249706
|
+
const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
|
|
249707
|
+
if (declared && isSourceDef2(declared))
|
|
249708
|
+
return false;
|
|
249709
|
+
}
|
|
249710
|
+
return true;
|
|
249711
|
+
}
|
|
249255
249712
|
function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
249256
249713
|
const filterMap = new Map;
|
|
249257
249714
|
const authorizeMap = new Map;
|
|
249258
|
-
const
|
|
249259
|
-
const
|
|
249260
|
-
const
|
|
249715
|
+
const misplacedAuthorize = [];
|
|
249716
|
+
const gatedSourceOwnAuthorizeNotes = new Set;
|
|
249717
|
+
const nearMissAuthorize = [];
|
|
249718
|
+
const sweptStructs = [];
|
|
249719
|
+
for (const obj of Object.values(modelDef.contents)) {
|
|
249720
|
+
if (!isSourceDef2(obj))
|
|
249721
|
+
continue;
|
|
249722
|
+
const struct = obj;
|
|
249723
|
+
sweptStructs.push(obj);
|
|
249724
|
+
for (const note of ownLevelNotes(struct.annotations)) {
|
|
249725
|
+
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
249726
|
+
gatedSourceOwnAuthorizeNotes.add(note);
|
|
249727
|
+
}
|
|
249728
|
+
}
|
|
249729
|
+
nearMissAuthorize.push(...collectAuthorizeNearMisses([
|
|
249730
|
+
...ownLevelNotes(struct.annotations),
|
|
249731
|
+
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
249732
|
+
].map((note) => note.text)));
|
|
249733
|
+
}
|
|
249734
|
+
for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
|
|
249735
|
+
const entry = value.entry;
|
|
249736
|
+
if (entry.type === "source_registry_reference")
|
|
249737
|
+
continue;
|
|
249738
|
+
if (!isSourceDef2(entry))
|
|
249739
|
+
continue;
|
|
249740
|
+
sweptStructs.push(entry);
|
|
249741
|
+
for (const note of ownLevelNotes(entry.annotations)) {
|
|
249742
|
+
if (containsAuthorizeAnnotationTag([note.text])) {
|
|
249743
|
+
gatedSourceOwnAuthorizeNotes.add(note);
|
|
249744
|
+
}
|
|
249745
|
+
}
|
|
249746
|
+
nearMissAuthorize.push(...collectAuthorizeNearMisses(ownLevelNotes(entry.annotations).map((note) => note.text)));
|
|
249747
|
+
}
|
|
249748
|
+
for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
|
|
249749
|
+
nearMissAuthorize.push(...collectAuthorizeNearMisses([
|
|
249750
|
+
...ownLevelNotes(struct.annotations),
|
|
249751
|
+
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
249752
|
+
].map((note) => note.text)));
|
|
249753
|
+
}
|
|
249754
|
+
{
|
|
249755
|
+
const folded = modelAnnotations(modelDef);
|
|
249756
|
+
nearMissAuthorize.push(...collectAuthorizeNearMisses([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
|
|
249757
|
+
}
|
|
249758
|
+
assertNoAuthorizeNearMisses(nearMissAuthorize);
|
|
249759
|
+
const authorizeOwnNotes = new Map;
|
|
249760
|
+
if (containsAuthorizeAnnotationTag((modelAnnotations(modelDef).notes ?? []).map((note) => note.text))) {
|
|
249761
|
+
misplacedAuthorize.push({ kind: "file" });
|
|
249762
|
+
}
|
|
249763
|
+
const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef2(obj)).map((sourceObj) => {
|
|
249261
249764
|
const struct = sourceObj;
|
|
249262
249765
|
const sourceName = struct.as || struct.name;
|
|
249263
249766
|
const annotations = annotationTexts(struct.annotations);
|
|
@@ -249295,36 +249798,44 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
249295
249798
|
}
|
|
249296
249799
|
const ownNotes = ownLevelNoteTexts(struct.annotations);
|
|
249297
249800
|
const ownGates = collectAuthorizeExprs(ownNotes);
|
|
249298
|
-
|
|
249299
|
-
|
|
249300
|
-
|
|
249301
|
-
const exprs = collectAuthorizeExprs(ownLevelNoteTexts(cur2));
|
|
249302
|
-
if (exprs.length > 0) {
|
|
249303
|
-
inheritedGates = exprs;
|
|
249304
|
-
break;
|
|
249305
|
-
}
|
|
249306
|
-
}
|
|
249307
|
-
}
|
|
249308
|
-
const effective = [
|
|
249309
|
-
...fileLevelAuthorize,
|
|
249310
|
-
...ownGates.length > 0 ? ownGates : inheritedGates
|
|
249311
|
-
];
|
|
249801
|
+
authorizeOwnNotes.set(sourceName, ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text])));
|
|
249802
|
+
const inheritedGroups = ownGates.length === 0 ? effectiveAncestorGateExprs(struct, modelDef) : [];
|
|
249803
|
+
const effectiveGroups = ownGates.length > 0 ? [ownGates] : inheritedGroups;
|
|
249312
249804
|
let authorize;
|
|
249313
|
-
if (
|
|
249314
|
-
authorizeMap.set(sourceName,
|
|
249315
|
-
authorize =
|
|
249316
|
-
}
|
|
249317
|
-
const ownEffective = [...fileLevelAuthorize, ...ownGates];
|
|
249318
|
-
if (ownEffective.length > 0) {
|
|
249319
|
-
ownAuthorizeSources.push({
|
|
249320
|
-
name: sourceName,
|
|
249321
|
-
authorize: ownEffective
|
|
249322
|
-
});
|
|
249805
|
+
if (effectiveGroups.length > 0) {
|
|
249806
|
+
authorizeMap.set(sourceName, effectiveGroups);
|
|
249807
|
+
authorize = effectiveGroups.flat();
|
|
249323
249808
|
}
|
|
249324
249809
|
const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
|
|
249325
249810
|
name: turtle.as || turtle.name,
|
|
249326
249811
|
annotations: annotationTexts(turtle.annotations)
|
|
249327
249812
|
}));
|
|
249813
|
+
for (const field of struct.fields) {
|
|
249814
|
+
const fieldAuthorizeNotes = ownLevelNotes(field.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
249815
|
+
if (fieldAuthorizeNotes.length === 0)
|
|
249816
|
+
continue;
|
|
249817
|
+
if (fieldAuthorizeNotes.every((note) => gatedSourceOwnAuthorizeNotes.has(note))) {
|
|
249818
|
+
continue;
|
|
249819
|
+
}
|
|
249820
|
+
const fieldName2 = field.as || field.name;
|
|
249821
|
+
if (isJoined2(field) && isSourceDef2(field)) {
|
|
249822
|
+
const joinedStruct = field;
|
|
249823
|
+
if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
|
|
249824
|
+
continue;
|
|
249825
|
+
}
|
|
249826
|
+
misplacedAuthorize.push({
|
|
249827
|
+
kind: "field",
|
|
249828
|
+
name: sourceName,
|
|
249829
|
+
fieldName: fieldName2
|
|
249830
|
+
});
|
|
249831
|
+
continue;
|
|
249832
|
+
}
|
|
249833
|
+
misplacedAuthorize.push({
|
|
249834
|
+
kind: "field",
|
|
249835
|
+
name: sourceName,
|
|
249836
|
+
fieldName: fieldName2
|
|
249837
|
+
});
|
|
249838
|
+
}
|
|
249328
249839
|
return {
|
|
249329
249840
|
name: sourceName,
|
|
249330
249841
|
annotations,
|
|
@@ -249334,20 +249845,33 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
249334
249845
|
authorize
|
|
249335
249846
|
};
|
|
249336
249847
|
});
|
|
249337
|
-
return {
|
|
249848
|
+
return {
|
|
249849
|
+
sources,
|
|
249850
|
+
filterMap,
|
|
249851
|
+
authorizeMap,
|
|
249852
|
+
misplacedAuthorize,
|
|
249853
|
+
authorizeOwnNotes
|
|
249854
|
+
};
|
|
249338
249855
|
}
|
|
249339
249856
|
function extractQueriesFromModelDef(modelDef) {
|
|
249340
249857
|
const isNamedQuery = (obj) => obj.type === "query";
|
|
249341
|
-
|
|
249858
|
+
const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
|
|
249859
|
+
const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
|
|
249860
|
+
kind: "query",
|
|
249861
|
+
name: queryObj.as || queryObj.name
|
|
249862
|
+
}));
|
|
249863
|
+
const queries = namedQueries.map((queryObj) => ({
|
|
249342
249864
|
name: queryObj.as || queryObj.name,
|
|
249343
249865
|
sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
|
|
249344
249866
|
annotations: annotationTexts(queryObj.annotations)
|
|
249345
249867
|
}));
|
|
249868
|
+
return { queries, misplacedAuthorize };
|
|
249346
249869
|
}
|
|
249347
249870
|
var init_source_extraction = __esm(() => {
|
|
249348
249871
|
init_annotations();
|
|
249349
249872
|
init_authorize();
|
|
249350
249873
|
init_filter();
|
|
249874
|
+
init_gate_registry_walk();
|
|
249351
249875
|
});
|
|
249352
249876
|
|
|
249353
249877
|
// src/service/model.ts
|
|
@@ -249355,7 +249879,7 @@ import {
|
|
|
249355
249879
|
API,
|
|
249356
249880
|
FixedConnectionMap,
|
|
249357
249881
|
InMemoryURLReader as InMemoryURLReader2,
|
|
249358
|
-
isSourceDef as
|
|
249882
|
+
isSourceDef as isSourceDef3,
|
|
249359
249883
|
MalloyConfig as MalloyConfig2,
|
|
249360
249884
|
MalloyError as MalloyError2,
|
|
249361
249885
|
Annotations as Annotations4,
|
|
@@ -249371,7 +249895,7 @@ import { readFileSync } from "fs";
|
|
|
249371
249895
|
import { createRequire as createRequire2 } from "module";
|
|
249372
249896
|
import * as path8 from "path";
|
|
249373
249897
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
249374
|
-
function
|
|
249898
|
+
function quoteMalloyIdentifier2(name) {
|
|
249375
249899
|
return "`" + (name ?? "").replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
|
|
249376
249900
|
}
|
|
249377
249901
|
function makeHydrationRuntime(malloyConfig, buildManifest) {
|
|
@@ -249414,6 +249938,7 @@ function hydrateNotebookCells(runtime, notebookCells) {
|
|
|
249414
249938
|
text: sc.text,
|
|
249415
249939
|
runnable,
|
|
249416
249940
|
modelMaterializer,
|
|
249941
|
+
modelDef: cellModelDef,
|
|
249417
249942
|
newSources: sc.newSources,
|
|
249418
249943
|
queryInfo: sc.queryInfo
|
|
249419
249944
|
};
|
|
@@ -249428,7 +249953,7 @@ function hydrateMarkdownOnlyCells(notebookCells) {
|
|
|
249428
249953
|
return { type: "code", text: sc.text };
|
|
249429
249954
|
});
|
|
249430
249955
|
}
|
|
249431
|
-
var MALLOY_VERSION,
|
|
249956
|
+
var MALLOY_VERSION, Model;
|
|
249432
249957
|
var init_model = __esm(() => {
|
|
249433
249958
|
init_telemetry();
|
|
249434
249959
|
init_materialization_metrics();
|
|
@@ -249451,6 +249976,7 @@ var init_model = __esm(() => {
|
|
|
249451
249976
|
init_json_utils();
|
|
249452
249977
|
init_query_metadata();
|
|
249453
249978
|
init_preaggregation_validation();
|
|
249979
|
+
init_gate_registry_walk();
|
|
249454
249980
|
init_source_extraction();
|
|
249455
249981
|
init_authorize_metrics();
|
|
249456
249982
|
MALLOY_VERSION = createRequire2(import.meta.url)("@malloydata/malloy/package.json").version;
|
|
@@ -249473,7 +249999,6 @@ var init_model = __esm(() => {
|
|
|
249473
249999
|
compilationError;
|
|
249474
250000
|
filterMap;
|
|
249475
250001
|
givens;
|
|
249476
|
-
fileLevelAuthorize = [];
|
|
249477
250002
|
declaredQueryMetadataMemo;
|
|
249478
250003
|
declaredSourceQueryMetadataMemo;
|
|
249479
250004
|
authorizeReferencedGivenNames = new Set;
|
|
@@ -249482,6 +250007,11 @@ var init_model = __esm(() => {
|
|
|
249482
250007
|
freshnessResolver;
|
|
249483
250008
|
preaggregateEntityIdResolver;
|
|
249484
250009
|
entryPointGatesBySource = new Map;
|
|
250010
|
+
gateRuntime;
|
|
250011
|
+
gateShapeCache = new Map;
|
|
250012
|
+
graftedMaterializerCache = new Map;
|
|
250013
|
+
static GRAFTED_MATERIALIZER_CACHE_MAX = 32;
|
|
250014
|
+
rowLevelFilteredRunnables = new WeakSet;
|
|
249485
250015
|
meter = publisherMeter();
|
|
249486
250016
|
queryExecutionHistogram = this.meter.createHistogram("malloy_model_query_duration", {
|
|
249487
250017
|
description: "How long it takes to execute a Malloy model query",
|
|
@@ -249505,11 +250035,6 @@ var init_model = __esm(() => {
|
|
|
249505
250035
|
this.compilationError = compilationError;
|
|
249506
250036
|
this.filterMap = filterMap ?? new Map;
|
|
249507
250037
|
this.givens = givens;
|
|
249508
|
-
try {
|
|
249509
|
-
this.fileLevelAuthorize = this.modelDef ? collectAuthorizeExprs((modelAnnotations(this.modelDef).notes ?? []).map((note) => note.text)) : [];
|
|
249510
|
-
} catch {
|
|
249511
|
-
this.fileLevelAuthorize = [];
|
|
249512
|
-
}
|
|
249513
250038
|
try {
|
|
249514
250039
|
this.entryPointGatesBySource = this.computeEntryPointGatesBySource();
|
|
249515
250040
|
} catch {
|
|
@@ -249539,8 +250064,64 @@ var init_model = __esm(() => {
|
|
|
249539
250064
|
getFilters(sourceName) {
|
|
249540
250065
|
return this.filterMap.get(sourceName) ?? [];
|
|
249541
250066
|
}
|
|
250067
|
+
setGateRuntime(runtime) {
|
|
250068
|
+
this.gateRuntime = runtime;
|
|
250069
|
+
}
|
|
250070
|
+
defaultGraftScope() {
|
|
250071
|
+
if (!this.modelDef || !this.modelMaterializer)
|
|
250072
|
+
return;
|
|
250073
|
+
return {
|
|
250074
|
+
modelDef: this.modelDef,
|
|
250075
|
+
materializer: this.modelMaterializer,
|
|
250076
|
+
cacheScope: "model"
|
|
250077
|
+
};
|
|
250078
|
+
}
|
|
250079
|
+
graftScopeForCell(cellIndex) {
|
|
250080
|
+
const cells = this.runnableNotebookCells;
|
|
250081
|
+
if (!cells)
|
|
250082
|
+
return;
|
|
250083
|
+
for (let i = cellIndex - 1;i >= 0; i--) {
|
|
250084
|
+
const prior = cells[i];
|
|
250085
|
+
if (prior.type === "code" && prior.modelDef && prior.modelMaterializer) {
|
|
250086
|
+
return {
|
|
250087
|
+
modelDef: prior.modelDef,
|
|
250088
|
+
materializer: prior.modelMaterializer,
|
|
250089
|
+
cacheScope: `cell:${i}`
|
|
250090
|
+
};
|
|
250091
|
+
}
|
|
250092
|
+
}
|
|
250093
|
+
return;
|
|
250094
|
+
}
|
|
250095
|
+
selfGraftScopeForCell(cellIndex) {
|
|
250096
|
+
const cell = this.runnableNotebookCells?.[cellIndex];
|
|
250097
|
+
if (!cell?.modelDef || !cell.modelMaterializer)
|
|
250098
|
+
return;
|
|
250099
|
+
return {
|
|
250100
|
+
modelDef: cell.modelDef,
|
|
250101
|
+
materializer: cell.modelMaterializer,
|
|
250102
|
+
cacheScope: `cell-self:${cellIndex}`
|
|
250103
|
+
};
|
|
250104
|
+
}
|
|
250105
|
+
async resolveNotebookCellGraftScope(cellIndex, runnable) {
|
|
250106
|
+
const selfScope = this.selfGraftScopeForCell(cellIndex);
|
|
250107
|
+
const earlierScope = this.graftScopeForCell(cellIndex);
|
|
250108
|
+
if (!earlierScope)
|
|
250109
|
+
return { graftScope: selfScope, usesOwnScope: true };
|
|
250110
|
+
const { struct, modelDef } = await this.resolveRunTargetStruct(runnable);
|
|
250111
|
+
if (struct && modelDef && this.resolveGraftTarget(struct, modelDef, earlierScope.modelDef)) {
|
|
250112
|
+
return { graftScope: earlierScope, usesOwnScope: false };
|
|
250113
|
+
}
|
|
250114
|
+
return { graftScope: selfScope, usesOwnScope: true };
|
|
250115
|
+
}
|
|
250116
|
+
givenDeclaredTypesCache;
|
|
249542
250117
|
givenDeclaredTypes() {
|
|
249543
|
-
|
|
250118
|
+
this.givenDeclaredTypesCache ??= new Map((this.givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
|
|
250119
|
+
return this.givenDeclaredTypesCache;
|
|
250120
|
+
}
|
|
250121
|
+
givenDeclaredDefaultsCache;
|
|
250122
|
+
givenDeclaredDefaults() {
|
|
250123
|
+
this.givenDeclaredDefaultsCache ??= new Map((this.givens ?? []).filter((g) => g.name != null && g.default != null).map((g) => [g.name, g.default]));
|
|
250124
|
+
return this.givenDeclaredDefaultsCache;
|
|
249544
250125
|
}
|
|
249545
250126
|
getAuthorize(sourceName) {
|
|
249546
250127
|
return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
|
|
@@ -249565,7 +250146,6 @@ var init_model = __esm(() => {
|
|
|
249565
250146
|
names.add(name);
|
|
249566
250147
|
}
|
|
249567
250148
|
};
|
|
249568
|
-
addExprs(this.fileLevelAuthorize);
|
|
249569
250149
|
for (const gates of this.entryPointGatesBySource.values()) {
|
|
249570
250150
|
for (const { exprs } of gates)
|
|
249571
250151
|
addExprs(exprs);
|
|
@@ -249578,7 +250158,7 @@ var init_model = __esm(() => {
|
|
|
249578
250158
|
if (!modelDef)
|
|
249579
250159
|
return byName;
|
|
249580
250160
|
for (const entry of Object.values(modelDef.contents)) {
|
|
249581
|
-
if (!
|
|
250161
|
+
if (!isSourceDef3(entry))
|
|
249582
250162
|
continue;
|
|
249583
250163
|
const name = entry.as ?? entry.name;
|
|
249584
250164
|
byName.set(name, this.collectEntryPointGates(entry, modelDef, new Set, true));
|
|
@@ -249586,27 +250166,68 @@ var init_model = __esm(() => {
|
|
|
249586
250166
|
return byName;
|
|
249587
250167
|
}
|
|
249588
250168
|
hasAuthorize() {
|
|
249589
|
-
return this.
|
|
249590
|
-
}
|
|
249591
|
-
|
|
249592
|
-
|
|
249593
|
-
|
|
249594
|
-
|
|
249595
|
-
|
|
250169
|
+
return this.sources?.some((s) => (s.authorize?.length ?? 0) > 0) ?? false;
|
|
250170
|
+
}
|
|
250171
|
+
anyAuthorizeNote;
|
|
250172
|
+
hasAnyAuthorizeNote() {
|
|
250173
|
+
if (this.anyAuthorizeNote !== undefined)
|
|
250174
|
+
return this.anyAuthorizeNote;
|
|
250175
|
+
this.anyAuthorizeNote = (() => {
|
|
250176
|
+
const modelDef = this.modelDef;
|
|
250177
|
+
if (!modelDef)
|
|
250178
|
+
return false;
|
|
250179
|
+
try {
|
|
250180
|
+
const structs = [];
|
|
250181
|
+
for (const obj of Object.values(modelDef.contents)) {
|
|
250182
|
+
if (isSourceDef3(obj))
|
|
250183
|
+
structs.push(obj);
|
|
250184
|
+
}
|
|
250185
|
+
for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
|
|
250186
|
+
const entry = value.entry;
|
|
250187
|
+
if (entry.type === "source_registry_reference")
|
|
250188
|
+
continue;
|
|
250189
|
+
if (isSourceDef3(entry))
|
|
250190
|
+
structs.push(entry);
|
|
250191
|
+
}
|
|
250192
|
+
structs.push(...derivedStructsReachable(structs, modelDef));
|
|
250193
|
+
for (const struct of structs) {
|
|
250194
|
+
if (containsAuthorizeAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
|
|
250195
|
+
return true;
|
|
250196
|
+
}
|
|
250197
|
+
for (const field of struct.fields) {
|
|
250198
|
+
if (containsAuthorizeAnnotationTag(annotationTexts(field.annotations) ?? [])) {
|
|
250199
|
+
return true;
|
|
250200
|
+
}
|
|
250201
|
+
}
|
|
250202
|
+
}
|
|
250203
|
+
return false;
|
|
250204
|
+
} catch {
|
|
250205
|
+
return true;
|
|
250206
|
+
}
|
|
250207
|
+
})();
|
|
250208
|
+
return this.anyAuthorizeNote;
|
|
249596
250209
|
}
|
|
249597
|
-
async assertAuthorized(sourceName, givens, bypassAuthorize = false) {
|
|
250210
|
+
async assertAuthorized(sourceName, givens, bypassAuthorize = false, graftScope = this.defaultGraftScope()) {
|
|
249598
250211
|
if (bypassAuthorize) {
|
|
249599
250212
|
this.noteAuthorizeBypass("source", sourceName);
|
|
249600
250213
|
return;
|
|
249601
250214
|
}
|
|
249602
250215
|
const gates = sourceName ? this.entryPointGatesBySource.get(sourceName) : undefined;
|
|
249603
250216
|
if (gates) {
|
|
249604
|
-
for (const
|
|
249605
|
-
await this.
|
|
250217
|
+
for (const entry of gates) {
|
|
250218
|
+
const resolution = this.modelDef ? await this.resolveGateShape(entry, this.modelDef, graftScope) : { kind: "given_only" };
|
|
250219
|
+
if (resolution.kind === "row_level")
|
|
250220
|
+
continue;
|
|
250221
|
+
if (resolution.kind === "deny") {
|
|
250222
|
+
recordRowLevelGateDecision("denied_by_gate");
|
|
250223
|
+
if (resolution.cause)
|
|
250224
|
+
recordRowLevelGateRejected(resolution.cause);
|
|
250225
|
+
throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
|
|
250226
|
+
}
|
|
250227
|
+
await this.assertAuthorizedExprs(entry.label, entry.exprs, givens, entry.selfContained);
|
|
249606
250228
|
}
|
|
249607
250229
|
return;
|
|
249608
250230
|
}
|
|
249609
|
-
await this.assertAuthorizedExprs(sourceName ?? "(query)", this.effectiveAuthorizeFor(sourceName), givens);
|
|
249610
250231
|
}
|
|
249611
250232
|
noteAuthorizeBypass(entryPoint, sourceName) {
|
|
249612
250233
|
recordAuthorizeBypass(entryPoint);
|
|
@@ -249617,7 +250238,7 @@ var init_model = __esm(() => {
|
|
|
249617
250238
|
packageName: this.packageName
|
|
249618
250239
|
});
|
|
249619
250240
|
}
|
|
249620
|
-
async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false
|
|
250241
|
+
async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false) {
|
|
249621
250242
|
if (exprs.length === 0)
|
|
249622
250243
|
return;
|
|
249623
250244
|
const deny = () => {
|
|
@@ -249627,7 +250248,7 @@ var init_model = __esm(() => {
|
|
|
249627
250248
|
deny();
|
|
249628
250249
|
let passed = false;
|
|
249629
250250
|
try {
|
|
249630
|
-
passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst
|
|
250251
|
+
passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens, this.givenDeclaredTypes(), { selfContainedFirst });
|
|
249631
250252
|
} catch (err) {
|
|
249632
250253
|
logger.debug("Authorize probe failed; denying", {
|
|
249633
250254
|
sourceName: label,
|
|
@@ -249640,28 +250261,102 @@ var init_model = __esm(() => {
|
|
|
249640
250261
|
deny();
|
|
249641
250262
|
}
|
|
249642
250263
|
async assertAuthorizedForAllSources(runnable, givens, bypassAuthorize = false) {
|
|
249643
|
-
|
|
249644
|
-
|
|
249645
|
-
|
|
249646
|
-
}
|
|
249647
|
-
const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
249648
|
-
await this.assertAuthorized(ownSourceName, givens);
|
|
249649
|
-
await this.assertAuthorizedFromCompiledRunnable(runnable, givens);
|
|
250264
|
+
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
250265
|
+
bypassAuthorize
|
|
250266
|
+
});
|
|
249650
250267
|
}
|
|
249651
|
-
async
|
|
250268
|
+
async collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
|
|
250269
|
+
const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
250270
|
+
if (!skipOwnSourceGate) {
|
|
250271
|
+
await this.assertAuthorized(ownSourceName, givens, false, graftScope);
|
|
250272
|
+
}
|
|
249652
250273
|
const { struct, modelDef, compositeResolvedSourceDef } = await this.resolveRunTargetStruct(runnable);
|
|
249653
250274
|
const seen = new Set;
|
|
249654
250275
|
const entryPointGates = this.collectEntryPointGates(struct, modelDef, seen, true);
|
|
249655
250276
|
if (compositeResolvedSourceDef && modelDef) {
|
|
249656
|
-
entryPointGates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true));
|
|
250277
|
+
entryPointGates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
|
|
249657
250278
|
}
|
|
249658
|
-
|
|
249659
|
-
|
|
249660
|
-
|
|
249661
|
-
|
|
249662
|
-
|
|
249663
|
-
}
|
|
249664
|
-
|
|
250279
|
+
return { entryPointGates, modelDef };
|
|
250280
|
+
}
|
|
250281
|
+
async assertAuthorizedFromCompiledRunnable(runnable, givens) {
|
|
250282
|
+
await this.authorizeAndBindRunnable(runnable, givens, {
|
|
250283
|
+
skipOwnSourceGate: true
|
|
250284
|
+
});
|
|
250285
|
+
}
|
|
250286
|
+
queryHadRowLevelFilterAttached(runnable) {
|
|
250287
|
+
return this.rowLevelFilteredRunnables.has(runnable);
|
|
250288
|
+
}
|
|
250289
|
+
async probeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
|
|
250290
|
+
const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
|
|
250291
|
+
const rowLevel = [];
|
|
250292
|
+
for (const entry of entryPointGates) {
|
|
250293
|
+
const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { kind: "given_only" };
|
|
250294
|
+
if (resolution.kind === "row_level") {
|
|
250295
|
+
rowLevel.push({
|
|
250296
|
+
label: entry.label,
|
|
250297
|
+
graftTarget: resolution.graftTarget,
|
|
250298
|
+
filterText: resolution.filterText,
|
|
250299
|
+
condition: resolution.condition
|
|
250300
|
+
});
|
|
250301
|
+
continue;
|
|
250302
|
+
}
|
|
250303
|
+
if (resolution.kind === "deny") {
|
|
250304
|
+
recordRowLevelGateDecision("denied_by_gate");
|
|
250305
|
+
if (resolution.cause)
|
|
250306
|
+
recordRowLevelGateRejected(resolution.cause);
|
|
250307
|
+
throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
|
|
250308
|
+
}
|
|
250309
|
+
await this.assertAuthorizedExprs(entry.label, entry.exprs, givens, entry.selfContained);
|
|
250310
|
+
}
|
|
250311
|
+
return rowLevel;
|
|
250312
|
+
}
|
|
250313
|
+
async queryEntryPointHasRowLevelGate(runnable) {
|
|
250314
|
+
try {
|
|
250315
|
+
const { struct, modelDef, compositeResolvedSourceDef } = await this.resolveRunTargetStruct(runnable);
|
|
250316
|
+
if (!modelDef)
|
|
250317
|
+
return true;
|
|
250318
|
+
const seen = new Set;
|
|
250319
|
+
const gates = this.collectEntryPointGates(struct, modelDef, seen, true);
|
|
250320
|
+
if (compositeResolvedSourceDef) {
|
|
250321
|
+
gates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
|
|
250322
|
+
}
|
|
250323
|
+
const graftScope = this.defaultGraftScope();
|
|
250324
|
+
for (const entry of gates) {
|
|
250325
|
+
const resolution = await this.resolveGateShape(entry, modelDef, graftScope);
|
|
250326
|
+
if (resolution.kind !== "given_only")
|
|
250327
|
+
return true;
|
|
250328
|
+
}
|
|
250329
|
+
return false;
|
|
250330
|
+
} catch {
|
|
250331
|
+
return true;
|
|
250332
|
+
}
|
|
250333
|
+
}
|
|
250334
|
+
async authorizeAndBindRunnable(runnable, givens, options) {
|
|
250335
|
+
if (options?.bypassAuthorize) {
|
|
250336
|
+
this.noteAuthorizeBypass("runnable", await this.resolveAuthorizeSourceFromRunnable(runnable));
|
|
250337
|
+
return runnable;
|
|
250338
|
+
}
|
|
250339
|
+
const graftScope = options?.graftScope ?? this.defaultGraftScope();
|
|
250340
|
+
const rowLevel = await this.probeEntryPointGates(runnable, givens, graftScope, options?.skipOwnSourceGate ?? false);
|
|
250341
|
+
if (rowLevel.length === 0)
|
|
250342
|
+
return runnable;
|
|
250343
|
+
if (!options?.recompile) {
|
|
250344
|
+
recordRowLevelGateDecision("denied_by_gate");
|
|
250345
|
+
throw new AccessDeniedError(`Access denied for source "${rowLevel[0].label}".`);
|
|
250346
|
+
}
|
|
250347
|
+
try {
|
|
250348
|
+
const graftedMaterializer = this.getOrBuildGraftedMaterializer(rowLevel, graftScope);
|
|
250349
|
+
const recompiled = options.recompile(graftedMaterializer);
|
|
250350
|
+
await this.assertGateLanded(recompiled, rowLevel);
|
|
250351
|
+
this.rowLevelFilteredRunnables.add(recompiled);
|
|
250352
|
+
return recompiled;
|
|
250353
|
+
} catch (err) {
|
|
250354
|
+
recordRowLevelGateDecision("denied_by_gate");
|
|
250355
|
+
logger.debug("Row-level authorize attach failed; denying", {
|
|
250356
|
+
modelPath: this.modelPath,
|
|
250357
|
+
error: err instanceof Error ? err.message : String(err)
|
|
250358
|
+
});
|
|
250359
|
+
throw new AccessDeniedError(`Access denied for source "${rowLevel[0].label}".`);
|
|
249665
250360
|
}
|
|
249666
250361
|
}
|
|
249667
250362
|
async resolveRunTargetStruct(runnable) {
|
|
@@ -249689,107 +250384,233 @@ var init_model = __esm(() => {
|
|
|
249689
250384
|
};
|
|
249690
250385
|
}
|
|
249691
250386
|
}
|
|
249692
|
-
gateExprsForOwnAnnotations(struct, modelDef) {
|
|
249693
|
-
const ownNotes =
|
|
250387
|
+
gateExprsForOwnAnnotations(struct, modelDef, excludeNotes = []) {
|
|
250388
|
+
const ownNotes = ownLevelNotes(struct.annotations).filter((note) => !excludeNotes.includes(note));
|
|
249694
250389
|
try {
|
|
249695
|
-
const own = collectAuthorizeExprs(ownNotes);
|
|
250390
|
+
const own = collectAuthorizeExprs(ownNotes.map((note) => note.text));
|
|
249696
250391
|
if (own.length > 0) {
|
|
249697
|
-
return {
|
|
249698
|
-
exprs: [...this.fileLevelAuthorize, ...own],
|
|
249699
|
-
fromAncestor: false,
|
|
249700
|
-
ambientPrefix: this.fileLevelAuthorize.length
|
|
249701
|
-
};
|
|
250392
|
+
return { exprs: own, fromAncestor: false };
|
|
249702
250393
|
}
|
|
249703
|
-
const ancestor =
|
|
249704
|
-
return {
|
|
249705
|
-
exprs: [...this.fileLevelAuthorize, ...ancestor],
|
|
249706
|
-
fromAncestor: ancestor.length > 0,
|
|
249707
|
-
ambientPrefix: this.fileLevelAuthorize.length
|
|
249708
|
-
};
|
|
250394
|
+
const ancestor = ancestorGateExprs(struct, modelDef);
|
|
250395
|
+
return { exprs: ancestor, fromAncestor: ancestor.length > 0 };
|
|
249709
250396
|
} catch {
|
|
249710
|
-
return { exprs: ["false"], fromAncestor: false
|
|
250397
|
+
return { exprs: ["false"], fromAncestor: false };
|
|
249711
250398
|
}
|
|
249712
250399
|
}
|
|
249713
|
-
|
|
249714
|
-
let inherited = struct.annotations?.inherits;
|
|
249715
|
-
for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
|
|
249716
|
-
const exprs2 = collectAuthorizeExprs(ownLevelNoteTexts(inherited));
|
|
249717
|
-
if (exprs2.length > 0)
|
|
249718
|
-
return exprs2;
|
|
249719
|
-
inherited = inherited.inherits;
|
|
249720
|
-
}
|
|
249721
|
-
if (inherited)
|
|
249722
|
-
return ["false"];
|
|
249723
|
-
seen.add(struct);
|
|
249724
|
-
if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
|
|
249725
|
-
return ["false"];
|
|
249726
|
-
const declared = this.resolveDeclaredSource(struct, modelDef);
|
|
249727
|
-
if (declared.kind === "unresolvable")
|
|
249728
|
-
return ["false"];
|
|
249729
|
-
if (declared.kind === "none" || seen.has(declared.source))
|
|
249730
|
-
return [];
|
|
249731
|
-
const exprs = collectAuthorizeExprs(ownLevelNoteTexts(declared.source.annotations));
|
|
249732
|
-
return exprs.length > 0 ? exprs : this.ancestorGateExprs(declared.source, modelDef, seen);
|
|
249733
|
-
}
|
|
249734
|
-
resolveDeclaredSource(struct, modelDef) {
|
|
249735
|
-
if (!modelDef)
|
|
249736
|
-
return { kind: "none" };
|
|
249737
|
-
let sawBrokenEntry = false;
|
|
249738
|
-
for (const id of [struct.referenceID, struct.sourceID]) {
|
|
249739
|
-
const entry = id ? modelDef.sourceRegistry?.[id]?.entry : undefined;
|
|
249740
|
-
if (!entry)
|
|
249741
|
-
continue;
|
|
249742
|
-
const declared = entry.type === "source_registry_reference" ? modelDef.contents[entry.name] : entry;
|
|
249743
|
-
if (declared === struct)
|
|
249744
|
-
continue;
|
|
249745
|
-
if (!declared || !isSourceDef2(declared)) {
|
|
249746
|
-
sawBrokenEntry = true;
|
|
249747
|
-
continue;
|
|
249748
|
-
}
|
|
249749
|
-
return { kind: "resolved", source: declared };
|
|
249750
|
-
}
|
|
249751
|
-
return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
|
|
249752
|
-
}
|
|
249753
|
-
collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false) {
|
|
250400
|
+
collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false, entryPointStruct = struct, excludeNotes = []) {
|
|
249754
250401
|
if (!struct || !modelDef || seen.has(struct))
|
|
249755
250402
|
return [];
|
|
249756
250403
|
seen.add(struct);
|
|
249757
250404
|
const results = [];
|
|
249758
250405
|
const label = struct.as ?? struct.name;
|
|
249759
|
-
const {
|
|
249760
|
-
exprs: ownExprs,
|
|
249761
|
-
fromAncestor,
|
|
249762
|
-
ambientPrefix
|
|
249763
|
-
} = this.gateExprsForOwnAnnotations(struct, modelDef);
|
|
250406
|
+
const { exprs: ownExprs, fromAncestor } = this.gateExprsForOwnAnnotations(struct, modelDef, excludeNotes);
|
|
249764
250407
|
if (ownExprs.length > 0) {
|
|
249765
250408
|
results.push({
|
|
249766
250409
|
label,
|
|
249767
250410
|
exprs: ownExprs,
|
|
249768
250411
|
selfContained: fromAncestor || !treatAsOwnGate,
|
|
249769
|
-
|
|
250412
|
+
struct: entryPointStruct
|
|
249770
250413
|
});
|
|
249771
250414
|
}
|
|
249772
250415
|
const duck = struct;
|
|
249773
250416
|
if (duck.type === "query_source") {
|
|
249774
|
-
const
|
|
249775
|
-
|
|
249776
|
-
|
|
249777
|
-
results.push(...this.collectEntryPointGates(base, modelDef, seen));
|
|
250417
|
+
const base = resolveQuerySourceBase(struct, modelDef);
|
|
250418
|
+
if (base) {
|
|
250419
|
+
results.push(...this.collectEntryPointGates(base, modelDef, seen, false, entryPointStruct));
|
|
249778
250420
|
} else {
|
|
249779
250421
|
results.push({
|
|
249780
250422
|
label,
|
|
249781
250423
|
exprs: ["false"],
|
|
249782
|
-
selfContained: true
|
|
249783
|
-
ambientPrefix: 0
|
|
250424
|
+
selfContained: true
|
|
249784
250425
|
});
|
|
249785
250426
|
}
|
|
249786
250427
|
const resolved = duck.query?.compositeResolvedSourceDef;
|
|
249787
250428
|
if (resolved) {
|
|
249788
|
-
results.push(...this.collectEntryPointGates(resolved, modelDef, seen));
|
|
250429
|
+
results.push(...this.collectEntryPointGates(resolved, modelDef, seen, false, entryPointStruct, base ? ownLevelNotes(base.annotations) : []));
|
|
249789
250430
|
}
|
|
249790
250431
|
}
|
|
249791
250432
|
return results;
|
|
249792
250433
|
}
|
|
250434
|
+
async resolveGateShape(entry, originModelDef, graftScope) {
|
|
250435
|
+
if (!entry.struct)
|
|
250436
|
+
return { kind: "given_only" };
|
|
250437
|
+
if (!graftScope) {
|
|
250438
|
+
logger.debug("Row-level gate has no graft scope to attach to (no scope was available at all, not the gate's own condition); denying", { modelPath: this.modelPath, label: entry.label });
|
|
250439
|
+
return { kind: "deny" };
|
|
250440
|
+
}
|
|
250441
|
+
const graftTarget = this.resolveGraftTarget(entry.struct, originModelDef, graftScope.modelDef);
|
|
250442
|
+
if (!graftTarget) {
|
|
250443
|
+
return this.classifyWithoutGraft(entry, graftScope.materializer);
|
|
250444
|
+
}
|
|
250445
|
+
const filterText = gateFilterText(entry.exprs);
|
|
250446
|
+
const cacheKey = `${graftScope.cacheScope}\x00${graftTarget}\x00${filterText}`;
|
|
250447
|
+
let cached2 = this.gateShapeCache.get(cacheKey);
|
|
250448
|
+
if (!cached2) {
|
|
250449
|
+
let condition;
|
|
250450
|
+
try {
|
|
250451
|
+
condition = await this.liftGateCondition(graftTarget, filterText, graftScope.materializer);
|
|
250452
|
+
} catch (err) {
|
|
250453
|
+
logger.debug("Row-level gate condition failed to lift; checking whether it is given-only before denying", {
|
|
250454
|
+
modelPath: this.modelPath,
|
|
250455
|
+
graftTarget,
|
|
250456
|
+
error: err instanceof Error ? err.message : String(err)
|
|
250457
|
+
});
|
|
250458
|
+
return this.classifyWithoutGraft(entry, graftScope.materializer);
|
|
250459
|
+
}
|
|
250460
|
+
const classification = classifyAuthorizeGate(condition, this.givenDeclaredTypes(), this.givenDeclaredDefaults());
|
|
250461
|
+
cached2 = { classification, condition };
|
|
250462
|
+
this.gateShapeCache.set(cacheKey, cached2);
|
|
250463
|
+
}
|
|
250464
|
+
if (cached2.classification.shape === "given_only") {
|
|
250465
|
+
return { kind: "given_only" };
|
|
250466
|
+
}
|
|
250467
|
+
if (cached2.classification.shape === "rejected") {
|
|
250468
|
+
return { kind: "deny", cause: cached2.classification.cause };
|
|
250469
|
+
}
|
|
250470
|
+
return {
|
|
250471
|
+
kind: "row_level",
|
|
250472
|
+
graftTarget,
|
|
250473
|
+
filterText,
|
|
250474
|
+
condition: cached2.condition
|
|
250475
|
+
};
|
|
250476
|
+
}
|
|
250477
|
+
async classifyWithoutGraft(entry, materializer) {
|
|
250478
|
+
try {
|
|
250479
|
+
await materializer.loadQuery(buildAuthorizeProbe(entry.exprs)).getPreparedQuery();
|
|
250480
|
+
return { kind: "given_only" };
|
|
250481
|
+
} catch (err) {
|
|
250482
|
+
logger.debug("Row-level gate has no attachable graft and does not compile as given-only; denying", {
|
|
250483
|
+
modelPath: this.modelPath,
|
|
250484
|
+
label: entry.label,
|
|
250485
|
+
error: err instanceof Error ? err.message : String(err)
|
|
250486
|
+
});
|
|
250487
|
+
return { kind: "deny" };
|
|
250488
|
+
}
|
|
250489
|
+
}
|
|
250490
|
+
resolveGraftTarget(struct, originModelDef, graftModelDef) {
|
|
250491
|
+
const direct = this.findContentsKey(struct, graftModelDef);
|
|
250492
|
+
if (direct)
|
|
250493
|
+
return direct;
|
|
250494
|
+
let current = struct;
|
|
250495
|
+
const seen = new Set([struct]);
|
|
250496
|
+
for (let depth = 0;depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
|
|
250497
|
+
const declared = resolveDeclaredSource(current, originModelDef);
|
|
250498
|
+
let next;
|
|
250499
|
+
if (declared.kind === "resolved" && !seen.has(declared.source)) {
|
|
250500
|
+
next = declared.source;
|
|
250501
|
+
} else if (declared.kind === "none") {
|
|
250502
|
+
next = this.findSourceByOwnAnnotationIdentity(current, graftModelDef, seen);
|
|
250503
|
+
}
|
|
250504
|
+
if (!next)
|
|
250505
|
+
return;
|
|
250506
|
+
const key = this.findContentsKey(next, graftModelDef);
|
|
250507
|
+
if (key)
|
|
250508
|
+
return key;
|
|
250509
|
+
seen.add(next);
|
|
250510
|
+
current = next;
|
|
250511
|
+
}
|
|
250512
|
+
return;
|
|
250513
|
+
}
|
|
250514
|
+
findContentsKey(struct, modelDef) {
|
|
250515
|
+
for (const [key, value] of Object.entries(modelDef.contents)) {
|
|
250516
|
+
if (value === struct)
|
|
250517
|
+
return key;
|
|
250518
|
+
}
|
|
250519
|
+
if (struct.sourceID) {
|
|
250520
|
+
for (const [key, value] of Object.entries(modelDef.contents)) {
|
|
250521
|
+
if (isSourceDef3(value) && value.sourceID === struct.sourceID) {
|
|
250522
|
+
return key;
|
|
250523
|
+
}
|
|
250524
|
+
}
|
|
250525
|
+
}
|
|
250526
|
+
return;
|
|
250527
|
+
}
|
|
250528
|
+
findSourceByOwnAnnotationIdentity(struct, modelDef, exclude) {
|
|
250529
|
+
const ownNotes = [
|
|
250530
|
+
...struct.annotations?.blockNotes ?? [],
|
|
250531
|
+
...struct.annotations?.notes ?? []
|
|
250532
|
+
];
|
|
250533
|
+
if (ownNotes.length === 0)
|
|
250534
|
+
return;
|
|
250535
|
+
for (const value of Object.values(modelDef.contents)) {
|
|
250536
|
+
if (!isSourceDef3(value) || value === struct || exclude.has(value)) {
|
|
250537
|
+
continue;
|
|
250538
|
+
}
|
|
250539
|
+
const candidateNotes = [
|
|
250540
|
+
...value.annotations?.blockNotes ?? [],
|
|
250541
|
+
...value.annotations?.notes ?? []
|
|
250542
|
+
];
|
|
250543
|
+
if (candidateNotes.some((note) => ownNotes.includes(note))) {
|
|
250544
|
+
return value;
|
|
250545
|
+
}
|
|
250546
|
+
}
|
|
250547
|
+
return;
|
|
250548
|
+
}
|
|
250549
|
+
async liftGateCondition(graftTarget, filterText, materializer) {
|
|
250550
|
+
const probe = materializer.loadQuery(buildRowLevelProbe(graftTarget, filterText));
|
|
250551
|
+
const prepared = await probe.getPreparedQuery();
|
|
250552
|
+
return liftProbeFilterCondition(prepared, `lifted probe for "${graftTarget}"`, filterText);
|
|
250553
|
+
}
|
|
250554
|
+
getOrBuildGraftedMaterializer(grafts, graftScope) {
|
|
250555
|
+
const key = `${graftScope.cacheScope}\x00` + grafts.map((g) => `${g.graftTarget}\x00${g.filterText}`).sort().join("\x01");
|
|
250556
|
+
const materializer = this.graftedMaterializerCache.get(key);
|
|
250557
|
+
if (materializer) {
|
|
250558
|
+
this.graftedMaterializerCache.delete(key);
|
|
250559
|
+
this.graftedMaterializerCache.set(key, materializer);
|
|
250560
|
+
return materializer;
|
|
250561
|
+
}
|
|
250562
|
+
const built = this.buildGraftedMaterializer(grafts, graftScope.modelDef);
|
|
250563
|
+
this.graftedMaterializerCache.set(key, built);
|
|
250564
|
+
while (this.graftedMaterializerCache.size > Model.GRAFTED_MATERIALIZER_CACHE_MAX) {
|
|
250565
|
+
const oldest = this.graftedMaterializerCache.keys().next();
|
|
250566
|
+
if (oldest.done)
|
|
250567
|
+
break;
|
|
250568
|
+
this.graftedMaterializerCache.delete(oldest.value);
|
|
250569
|
+
}
|
|
250570
|
+
return built;
|
|
250571
|
+
}
|
|
250572
|
+
buildGraftedMaterializer(grafts, modelDef) {
|
|
250573
|
+
if (!this.gateRuntime) {
|
|
250574
|
+
throw new Error("no retained runtime to graft a row-level gate through");
|
|
250575
|
+
}
|
|
250576
|
+
const copy = structuredClone(modelDef);
|
|
250577
|
+
for (const { graftTarget, condition } of grafts) {
|
|
250578
|
+
const target = copy.contents[graftTarget];
|
|
250579
|
+
if (!target || !isSourceDef3(target)) {
|
|
250580
|
+
throw new Error(`graft target "${graftTarget}" is not a source in this model`);
|
|
250581
|
+
}
|
|
250582
|
+
target.filterList = [...target.filterList ?? [], condition];
|
|
250583
|
+
}
|
|
250584
|
+
return this.gateRuntime._loadModelFromModelDef(copy);
|
|
250585
|
+
}
|
|
250586
|
+
async assertGateLanded(recompiled, grafts) {
|
|
250587
|
+
const prepared = await recompiled.getPreparedQuery();
|
|
250588
|
+
const modelDef = prepared._modelDef ?? this.modelDef;
|
|
250589
|
+
const structRef = prepared._query?.structRef;
|
|
250590
|
+
const resolvedRef = typeof structRef === "string" ? modelDef?.contents[structRef] : structRef;
|
|
250591
|
+
const struct = resolvedRef && typeof resolvedRef === "object" ? resolvedRef : undefined;
|
|
250592
|
+
for (const { condition } of grafts) {
|
|
250593
|
+
if (!condition.code || !this.filterListContainsCode(struct, modelDef, condition.code, 0)) {
|
|
250594
|
+
throw new Error("a row-level gate condition did not land on the recompiled query");
|
|
250595
|
+
}
|
|
250596
|
+
}
|
|
250597
|
+
}
|
|
250598
|
+
static MAX_GATE_PROOF_DEPTH = 8;
|
|
250599
|
+
filterListContainsCode(struct, modelDef, code, depth) {
|
|
250600
|
+
if (!struct || depth > Model.MAX_GATE_PROOF_DEPTH)
|
|
250601
|
+
return false;
|
|
250602
|
+
if (struct.filterList?.some((f) => f.code === code))
|
|
250603
|
+
return true;
|
|
250604
|
+
const duck = struct;
|
|
250605
|
+
if (duck.type === "query_source" && modelDef) {
|
|
250606
|
+
const ref = duck.query?.structRef;
|
|
250607
|
+
const base = typeof ref === "string" ? modelDef.contents[ref] : ref;
|
|
250608
|
+
if (base && isSourceDef3(base)) {
|
|
250609
|
+
return this.filterListContainsCode(base, modelDef, code, depth + 1);
|
|
250610
|
+
}
|
|
250611
|
+
}
|
|
250612
|
+
return false;
|
|
250613
|
+
}
|
|
249793
250614
|
async assertAuthorizedForText(text, givens) {
|
|
249794
250615
|
await this.assertAuthorized(extractRunTargetSourceName(text), givens);
|
|
249795
250616
|
}
|
|
@@ -249842,8 +250663,22 @@ var init_model = __esm(() => {
|
|
|
249842
250663
|
const sourceResult = Model.getSources(modelDef, givens);
|
|
249843
250664
|
sources = sourceResult.sources;
|
|
249844
250665
|
filterMap = sourceResult.filterMap;
|
|
249845
|
-
|
|
249846
|
-
|
|
250666
|
+
const queryResult = Model.getQueries(modelDef);
|
|
250667
|
+
queries = queryResult.queries;
|
|
250668
|
+
assertNoMisplacedAuthorizeAnnotations([
|
|
250669
|
+
...sourceResult.misplacedAuthorize,
|
|
250670
|
+
...queryResult.misplacedAuthorize
|
|
250671
|
+
]);
|
|
250672
|
+
const declaredGivenTypes = new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
|
|
250673
|
+
const declaredGivenDefaults = new Map((givens ?? []).filter((g) => g.name != null && g.default != null).map((g) => [g.name, g.default]));
|
|
250674
|
+
await validateAuthorizeProbes(modelMaterializer, {
|
|
250675
|
+
authorizeMap: sourceResult.authorizeMap,
|
|
250676
|
+
declaredTypes: declaredGivenTypes,
|
|
250677
|
+
declaredDefaults: declaredGivenDefaults,
|
|
250678
|
+
authorizeOwnNotes: sourceResult.authorizeOwnNotes,
|
|
250679
|
+
onRowLevelGateRejected: recordRowLevelGateRejected,
|
|
250680
|
+
onRowLevelGateUnexpressible: (sourceName, detail) => logger.warn("Row-level #(authorize) gate not expressible at this entry point; every query against it will be denied", { packageName, modelPath, sourceName, detail })
|
|
250681
|
+
});
|
|
249847
250682
|
const imports = modelDef.imports || [];
|
|
249848
250683
|
const importedSourceNames = new Set;
|
|
249849
250684
|
for (const importLocation of imports) {
|
|
@@ -249873,7 +250708,9 @@ var init_model = __esm(() => {
|
|
|
249873
250708
|
}
|
|
249874
250709
|
}
|
|
249875
250710
|
}
|
|
249876
|
-
|
|
250711
|
+
const model = new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined, filterMap, givens);
|
|
250712
|
+
model.setGateRuntime(runtime);
|
|
250713
|
+
return model;
|
|
249877
250714
|
} catch (error) {
|
|
249878
250715
|
let computedError = error;
|
|
249879
250716
|
if (error instanceof Error && error.stack) {
|
|
@@ -249898,6 +250735,9 @@ var init_model = __esm(() => {
|
|
|
249898
250735
|
const sourceInfos = data.sourceInfos;
|
|
249899
250736
|
const givens = data.givens;
|
|
249900
250737
|
const filterMap = data.filterMap ? new Map(data.filterMap) : undefined;
|
|
250738
|
+
for (const warning of data.authorizeWarnings ?? []) {
|
|
250739
|
+
logger.warn(warning, { packageName, modelPath: data.modelPath });
|
|
250740
|
+
}
|
|
249901
250741
|
if (!modelDef) {
|
|
249902
250742
|
return new Model(packageName, data.modelPath, dataStyles, data.modelType, undefined, undefined, sources, queries, sourceInfos, data.modelType === "notebook" ? hydrateMarkdownOnlyCells(data.notebookCells) : undefined, undefined, filterMap, givens, modelInfo);
|
|
249903
250743
|
}
|
|
@@ -249905,6 +250745,7 @@ var init_model = __esm(() => {
|
|
|
249905
250745
|
const modelMaterializer = runtime._loadModelFromModelDef(modelDef);
|
|
249906
250746
|
const runnableNotebookCells = data.modelType === "notebook" ? hydrateNotebookCells(runtime, data.notebookCells) : undefined;
|
|
249907
250747
|
const model = new Model(packageName, data.modelPath, dataStyles, data.modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, undefined, filterMap, givens, modelInfo);
|
|
250748
|
+
model.setGateRuntime(runtime);
|
|
249908
250749
|
return model;
|
|
249909
250750
|
}
|
|
249910
250751
|
static fromCompilationError(packageName, modelPath, modelType, error) {
|
|
@@ -250117,7 +250958,7 @@ var init_model = __esm(() => {
|
|
|
250117
250958
|
}
|
|
250118
250959
|
targets.push({
|
|
250119
250960
|
label: query.name,
|
|
250120
|
-
queryString: `run: ${
|
|
250961
|
+
queryString: `run: ${quoteMalloyIdentifier2(query.name)}`
|
|
250121
250962
|
});
|
|
250122
250963
|
}
|
|
250123
250964
|
for (const source of this.sources ?? []) {
|
|
@@ -250127,7 +250968,7 @@ var init_model = __esm(() => {
|
|
|
250127
250968
|
}
|
|
250128
250969
|
targets.push({
|
|
250129
250970
|
label: `${source.name} -> ${view.name}`,
|
|
250130
|
-
queryString: `run: ${
|
|
250971
|
+
queryString: `run: ${quoteMalloyIdentifier2(source.name)} -> ${quoteMalloyIdentifier2(view.name)}`
|
|
250131
250972
|
});
|
|
250132
250973
|
}
|
|
250133
250974
|
}
|
|
@@ -250307,6 +251148,7 @@ var init_model = __esm(() => {
|
|
|
250307
251148
|
}
|
|
250308
251149
|
let runnable;
|
|
250309
251150
|
let liveRunnable;
|
|
251151
|
+
let queryString;
|
|
250310
251152
|
let serveVirtualMap;
|
|
250311
251153
|
let serveShapeBindings = [];
|
|
250312
251154
|
let servedFrom;
|
|
@@ -250337,13 +251179,12 @@ var init_model = __esm(() => {
|
|
|
250337
251179
|
throw err;
|
|
250338
251180
|
}
|
|
250339
251181
|
}
|
|
250340
|
-
let queryString;
|
|
250341
251182
|
if (!sourceName && !queryName && query) {
|
|
250342
251183
|
queryString = `
|
|
250343
251184
|
` + query;
|
|
250344
251185
|
} else if (queryName && !query) {
|
|
250345
251186
|
queryString = `
|
|
250346
|
-
run: ${sourceName ? `${
|
|
251187
|
+
run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMalloyIdentifier2(queryName)}`;
|
|
250347
251188
|
} else {
|
|
250348
251189
|
const endTime = performance.now();
|
|
250349
251190
|
const executionTime2 = endTime - startTime;
|
|
@@ -250368,7 +251209,10 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250368
251209
|
}
|
|
250369
251210
|
runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
|
|
250370
251211
|
liveRunnable = runnable;
|
|
250371
|
-
|
|
251212
|
+
const storageRoutingPossible = getPersistStorageMode() === "on" && this.serveBindings.length > 0 && !!this.serveDestinationConfig;
|
|
251213
|
+
const preaggServeMaterializer = this.preaggregateServeMaterializer;
|
|
251214
|
+
const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && this.hasAnyAuthorizeNote() && await this.queryEntryPointHasRowLevelGate(runnable);
|
|
251215
|
+
if (storageRoutingPossible && !routingBlockedByRowLevelGate) {
|
|
250372
251216
|
try {
|
|
250373
251217
|
const shaped = await this.loadServeShapeQuery(queryString);
|
|
250374
251218
|
runnable = shaped.runnable;
|
|
@@ -250388,9 +251232,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250388
251232
|
});
|
|
250389
251233
|
}
|
|
250390
251234
|
}
|
|
250391
|
-
if (
|
|
251235
|
+
if (preaggServeMaterializer && !routingBlockedByRowLevelGate && !serveVirtualMap) {
|
|
250392
251236
|
try {
|
|
250393
|
-
const candidate =
|
|
251237
|
+
const candidate = preaggServeMaterializer.loadRestrictedQuery(queryString);
|
|
250394
251238
|
await candidate.getSQL({
|
|
250395
251239
|
givens: querySurfaceGivens,
|
|
250396
251240
|
buildManifest
|
|
@@ -250430,7 +251274,10 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250430
251274
|
if (boundary === "deferred") {
|
|
250431
251275
|
this.assertQueryBoundaryCompiled(compiledSource, query);
|
|
250432
251276
|
}
|
|
250433
|
-
await this.
|
|
251277
|
+
runnable = await this.authorizeAndBindRunnable(runnable, givens ?? {}, {
|
|
251278
|
+
recompile: (mm) => mm.loadRestrictedQuery(queryString),
|
|
251279
|
+
bypassAuthorize
|
|
251280
|
+
});
|
|
250434
251281
|
const maxRows = getMaxQueryRows();
|
|
250435
251282
|
const maxBytes = getMaxResponseBytes();
|
|
250436
251283
|
const effectiveBuildManifest = serveVirtualMap ? undefined : preaggRouted ? buildManifest : liveBuildManifest;
|
|
@@ -250530,6 +251377,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250530
251377
|
servedFrom = "live_fallback";
|
|
250531
251378
|
executionTime = performance.now() - startTime;
|
|
250532
251379
|
}
|
|
251380
|
+
if (this.queryHadRowLevelFilterAttached(runnable) && queryResults.totalRows === 0) {
|
|
251381
|
+
recordRowLevelGateDecision("empty_after_filter");
|
|
251382
|
+
}
|
|
250533
251383
|
assertWithinModelRowLimit(queryResults.totalRows, maxRows, "model_query");
|
|
250534
251384
|
const wrappedResult = API.util.wrapResult(queryResults);
|
|
250535
251385
|
const serializedResult = stringifyQueryResponse(responseShape === "compact" ? queryResults.data.value : wrappedResult, queryResults.totalRows, maxBytes, "model_query", responseShape === "compact" ? bigIntReplacer : undefined);
|
|
@@ -250626,7 +251476,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250626
251476
|
return;
|
|
250627
251477
|
try {
|
|
250628
251478
|
const entry = this.modelDef.contents?.[sourceName];
|
|
250629
|
-
if (!entry || !
|
|
251479
|
+
if (!entry || !isSourceDef3(entry))
|
|
250630
251480
|
return;
|
|
250631
251481
|
const def = entry;
|
|
250632
251482
|
if (!def.annotations)
|
|
@@ -250694,26 +251544,35 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250694
251544
|
text: cell.text
|
|
250695
251545
|
};
|
|
250696
251546
|
}
|
|
250697
|
-
if (cell.runnable) {
|
|
250698
|
-
await this.assertAuthorizedForAllSources(cell.runnable, givens ?? {});
|
|
250699
|
-
}
|
|
250700
251547
|
let queryName = undefined;
|
|
250701
251548
|
let queryResult = undefined;
|
|
250702
251549
|
if (cell.runnable) {
|
|
250703
251550
|
try {
|
|
250704
251551
|
let runnableToExecute = cell.runnable;
|
|
250705
|
-
|
|
250706
|
-
|
|
250707
|
-
|
|
250708
|
-
|
|
250709
|
-
|
|
250710
|
-
|
|
250711
|
-
|
|
250712
|
-
|
|
250713
|
-
|
|
250714
|
-
|
|
250715
|
-
|
|
250716
|
-
|
|
251552
|
+
let textToExecute = cell.text;
|
|
251553
|
+
const { graftScope, usesOwnScope } = await this.resolveNotebookCellGraftScope(cellIndex, cell.runnable);
|
|
251554
|
+
if (!graftScope) {
|
|
251555
|
+
logger.debug("Notebook cell has no graft scope to attach a row-level gate against (no earlier code cell, and this cell has no compiled model of its own)", { modelPath: this.modelPath, cellIndex });
|
|
251556
|
+
}
|
|
251557
|
+
const effectiveSource = !bypassFilters && cell.modelMaterializer ? extractRunTargetSourceName(cell.text) : undefined;
|
|
251558
|
+
const cellFilters = effectiveSource ? this.getFilters(effectiveSource) : [];
|
|
251559
|
+
if (cell.modelMaterializer && cellFilters.length > 0) {
|
|
251560
|
+
await this.probeEntryPointGates(cell.runnable, givens ?? {}, graftScope);
|
|
251561
|
+
}
|
|
251562
|
+
const filterClause = cellFilters.length > 0 ? buildFilterClause(cellFilters, filterParams ?? {}) : undefined;
|
|
251563
|
+
if (filterClause && cell.modelMaterializer) {
|
|
251564
|
+
textToExecute = injectFilterRefinement(cell.text, filterClause);
|
|
251565
|
+
runnableToExecute = cell.modelMaterializer.loadQuery(textToExecute);
|
|
251566
|
+
}
|
|
251567
|
+
if (cell.modelMaterializer) {
|
|
251568
|
+
const textForRecompile = textToExecute;
|
|
251569
|
+
const queryDefForOwnScopeRepoint = usesOwnScope ? (await runnableToExecute.getPreparedQuery())._query : undefined;
|
|
251570
|
+
runnableToExecute = await this.authorizeAndBindRunnable(runnableToExecute, givens ?? {}, {
|
|
251571
|
+
recompile: usesOwnScope ? (mm) => mm._loadQueryFromQueryDef(queryDefForOwnScopeRepoint) : (mm) => mm.loadQuery(textForRecompile),
|
|
251572
|
+
graftScope
|
|
251573
|
+
});
|
|
251574
|
+
} else {
|
|
251575
|
+
await this.assertAuthorizedForAllSources(runnableToExecute, givens ?? {});
|
|
250717
251576
|
}
|
|
250718
251577
|
const cellMaxRows = getMaxQueryRows();
|
|
250719
251578
|
const cellMaxBytes = getMaxResponseBytes();
|
|
@@ -250737,6 +251596,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250737
251596
|
});
|
|
250738
251597
|
const query = (await runnableToExecute.getPreparedQuery())._query;
|
|
250739
251598
|
queryName = query.as || query.name;
|
|
251599
|
+
if (result?._queryResult && result.totalRows === 0 && this.queryHadRowLevelFilterAttached(runnableToExecute)) {
|
|
251600
|
+
recordRowLevelGateDecision("empty_after_filter");
|
|
251601
|
+
}
|
|
250740
251602
|
if (result?._queryResult) {
|
|
250741
251603
|
assertWithinModelRowLimit(result.totalRows, cellMaxRows, "notebook_cell");
|
|
250742
251604
|
}
|
|
@@ -250745,6 +251607,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250745
251607
|
assertWithinModelByteLimit(queryResult, cellMaxBytes, "notebook_cell");
|
|
250746
251608
|
}
|
|
250747
251609
|
} catch (error) {
|
|
251610
|
+
if (error instanceof AccessDeniedError) {
|
|
251611
|
+
throw error;
|
|
251612
|
+
}
|
|
250748
251613
|
if (error instanceof FilterValidationError) {
|
|
250749
251614
|
throw new BadRequestError(error.message);
|
|
250750
251615
|
}
|
|
@@ -250824,14 +251689,23 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250824
251689
|
return malloyConfig;
|
|
250825
251690
|
}
|
|
250826
251691
|
static getQueries(modelDef) {
|
|
250827
|
-
|
|
251692
|
+
const { queries, misplacedAuthorize } = extractQueriesFromModelDef(modelDef);
|
|
251693
|
+
return { queries, misplacedAuthorize };
|
|
250828
251694
|
}
|
|
250829
251695
|
static getSources(modelDef, givens) {
|
|
250830
|
-
const {
|
|
251696
|
+
const {
|
|
251697
|
+
sources,
|
|
251698
|
+
filterMap,
|
|
251699
|
+
authorizeMap,
|
|
251700
|
+
misplacedAuthorize,
|
|
251701
|
+
authorizeOwnNotes
|
|
251702
|
+
} = extractSourcesFromModelDef(modelDef, givens, (sourceName, err) => logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err }));
|
|
250831
251703
|
return {
|
|
250832
251704
|
sources,
|
|
250833
251705
|
filterMap,
|
|
250834
|
-
|
|
251706
|
+
authorizeMap,
|
|
251707
|
+
misplacedAuthorize,
|
|
251708
|
+
authorizeOwnNotes
|
|
250835
251709
|
};
|
|
250836
251710
|
}
|
|
250837
251711
|
static async getModelMaterializer(runtime, importBaseURL, modelURL, modelPath) {
|
|
@@ -250932,6 +251806,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
|
|
|
250932
251806
|
text: stmt.text,
|
|
250933
251807
|
runnable,
|
|
250934
251808
|
modelMaterializer: localMM,
|
|
251809
|
+
modelDef: currentModelDef,
|
|
250935
251810
|
newSources,
|
|
250936
251811
|
queryInfo
|
|
250937
251812
|
};
|
|
@@ -295572,6 +296447,8 @@ class MaterializationService {
|
|
|
295572
296447
|
const destination = resolveStorageDestination(persistSource);
|
|
295573
296448
|
if (destination) {
|
|
295574
296449
|
assertMaterializationEligible(persistSource);
|
|
296450
|
+
} else {
|
|
296451
|
+
assertColocatedPersistNotAuthorizeGated(persistSource, persistSource.name, compiled.preaggregatePlans?.[persistSource.sourceID] ? "preaggregate" : "persist");
|
|
295575
296452
|
}
|
|
295576
296453
|
const sourceEntityId = computeSourceEntityId(persistSource, compiled.connectionDigests);
|
|
295577
296454
|
if (seen.has(sourceEntityId))
|
|
@@ -295834,6 +296711,8 @@ class MaterializationService {
|
|
|
295834
296711
|
const orchestratedInstruction = bySourceID.get(persistSource.sourceID);
|
|
295835
296712
|
if (orchestratedInstruction?.destination && getPersistStorageMode() !== "off") {
|
|
295836
296713
|
assertMaterializationEligible(persistSource);
|
|
296714
|
+
} else {
|
|
296715
|
+
assertColocatedPersistNotAuthorizeGated(persistSource, persistSource.name, compiled.preaggregatePlans?.[persistSource.sourceID] ? "preaggregate" : "persist");
|
|
295837
296716
|
}
|
|
295838
296717
|
const sourceEntityId = computeSourceEntityId(persistSource, connectionDigests);
|
|
295839
296718
|
const instruction = orchestratedInstruction ?? bySourceEntityId.get(sourceEntityId);
|