@malloy-publisher/server 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/api-doc.yaml +125 -59
- package/dist/app/assets/{EnvironmentPage-B02geyxh.js → EnvironmentPage-ufeWfqnK.js} +1 -1
- package/dist/app/assets/{HomePage-BdKzZEto.js → HomePage-CN_FgmtD.js} +1 -1
- package/dist/app/assets/{LightMode-BRAs1k98.js → LightMode-BFbC4W45.js} +1 -1
- package/dist/app/assets/{MainPage-D9XZbGvk.js → MainPage-BcCyzakx.js} +1 -1
- package/dist/app/assets/{ModelPage-B-U47DzF.js → ModelPage-wHHnxoih.js} +2 -2
- package/dist/app/assets/{PackagePage-CDp9A3_k.js → PackagePage-DwdO-2dp.js} +1 -1
- package/dist/app/assets/{RenderedResult-CTD2Ej2S.es-BZsZAiwo.js → RenderedResult-CYyhQekT.es-DBHF8EwO.js} +2 -2
- package/dist/app/assets/{RouteError-CSsLG99c.js → RouteError-Blt6a2Za.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-C4u5l49i.js → ThemeEditorPage-Cb-2YbfG.js} +1 -1
- package/dist/app/assets/{ToggleButtonGroup-IBhUYkv_.js → ToggleButtonGroup-BuhURn8v.js} +1 -1
- package/dist/app/assets/{index-DOXSuS-a.js → index-BELjGKhI.js} +1 -1
- package/dist/app/assets/{index--DAcpR5Y.js → index-BJyLY8XQ.js} +1 -1
- package/dist/app/assets/{index-BKNRDF5y.js → index-DCizS2BO.js} +1 -1
- package/dist/app/assets/{index-Bd7WYNw_.js → index-Dz4y_2PE.js} +4 -4
- package/dist/app/assets/{index-CshLRozl.js → index-kt4oTEGz.js} +1 -1
- package/dist/app/assets/{index.es-Bj77sE9l.js → index.es-u5PRRTzT.js} +4 -4
- package/dist/app/index.html +1 -1
- package/dist/cpufeatures-1yrn0vtw.node +0 -0
- package/dist/package_load_worker.mjs +747 -357
- package/dist/server.mjs +1450 -628
- package/dist/{sshcrypto-xqan60jb.node → sshcrypto-cqb52edz.node} +0 -0
- package/package.json +1 -1
|
@@ -939,7 +939,7 @@ import os from "os";
|
|
|
939
939
|
function normalizeModelPath(p) {
|
|
940
940
|
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
941
941
|
}
|
|
942
|
-
var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
|
|
942
|
+
var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
|
|
943
943
|
var init_constants = __esm(() => {
|
|
944
944
|
TEMP_DIR_PATH = os.tmpdir();
|
|
945
945
|
});
|
|
@@ -13103,6 +13103,29 @@ var init_errors = __esm(() => {
|
|
|
13103
13103
|
};
|
|
13104
13104
|
});
|
|
13105
13105
|
|
|
13106
|
+
// src/service/authorize_routes.ts
|
|
13107
|
+
function canonicalAuthorizeRoute(route) {
|
|
13108
|
+
switch (route) {
|
|
13109
|
+
case ACCESS_FILTER_ROUTE:
|
|
13110
|
+
return ACCESS_FILTER_ROUTE;
|
|
13111
|
+
case AUTHORIZE_ROUTE:
|
|
13112
|
+
return AUTHORIZE_ROUTE;
|
|
13113
|
+
default:
|
|
13114
|
+
return;
|
|
13115
|
+
}
|
|
13116
|
+
}
|
|
13117
|
+
var AUTHORIZE_ROUTE = "authorize", ACCESS_FILTER_ROUTE = "access_filter", RECOGNIZED_AUTHORIZE_SPELLINGS, CANONICAL_AUTHORIZE_ROUTES;
|
|
13118
|
+
var init_authorize_routes = __esm(() => {
|
|
13119
|
+
RECOGNIZED_AUTHORIZE_SPELLINGS = [
|
|
13120
|
+
ACCESS_FILTER_ROUTE,
|
|
13121
|
+
AUTHORIZE_ROUTE
|
|
13122
|
+
];
|
|
13123
|
+
CANONICAL_AUTHORIZE_ROUTES = [
|
|
13124
|
+
ACCESS_FILTER_ROUTE,
|
|
13125
|
+
AUTHORIZE_ROUTE
|
|
13126
|
+
];
|
|
13127
|
+
});
|
|
13128
|
+
|
|
13106
13129
|
// src/service/authorize.ts
|
|
13107
13130
|
import { payloadOf, routeOf } from "@malloydata/malloy";
|
|
13108
13131
|
function noteRoute(text) {
|
|
@@ -13111,8 +13134,61 @@ function noteRoute(text) {
|
|
|
13111
13134
|
function notePayload(text) {
|
|
13112
13135
|
return payloadOf({ value: text.trimStart() }) ?? "";
|
|
13113
13136
|
}
|
|
13137
|
+
function nearMissRouteNames(route) {
|
|
13138
|
+
switch (route) {
|
|
13139
|
+
case AUTHORIZE_ROUTE:
|
|
13140
|
+
return [
|
|
13141
|
+
"authorize",
|
|
13142
|
+
"source_authorize",
|
|
13143
|
+
"source-authorize",
|
|
13144
|
+
"sourceauthorize",
|
|
13145
|
+
"authorize_source",
|
|
13146
|
+
"authorize-source",
|
|
13147
|
+
"authorizesource"
|
|
13148
|
+
];
|
|
13149
|
+
case ACCESS_FILTER_ROUTE:
|
|
13150
|
+
return [
|
|
13151
|
+
"access_filter",
|
|
13152
|
+
"access-filter",
|
|
13153
|
+
"accessfilter",
|
|
13154
|
+
"filter_access",
|
|
13155
|
+
"filter-access",
|
|
13156
|
+
"filteraccess",
|
|
13157
|
+
"row_authorize",
|
|
13158
|
+
"row-authorize",
|
|
13159
|
+
"rowauthorize",
|
|
13160
|
+
"authorize_row",
|
|
13161
|
+
"authorize-row",
|
|
13162
|
+
"authorizerow"
|
|
13163
|
+
];
|
|
13164
|
+
default:
|
|
13165
|
+
return [route];
|
|
13166
|
+
}
|
|
13167
|
+
}
|
|
13168
|
+
function nearMissWordAlternation(route) {
|
|
13169
|
+
const names = nearMissRouteNames(route);
|
|
13170
|
+
if (names.length === 0) {
|
|
13171
|
+
throw new Error(`No near-miss spellings for authorize route \`${route}\` — an empty ` + "alternation matches every malformed-prefix note, which would " + "fail package loads over annotations nobody wrote.");
|
|
13172
|
+
}
|
|
13173
|
+
return names.map((name) => name.replace(REGEXP_SPECIAL, "\\$&")).join("|");
|
|
13174
|
+
}
|
|
13175
|
+
function motlyAuthorizePayloadPattern(route) {
|
|
13176
|
+
return new RegExp(`^[ \\t]*[([{<][ \\t]*(?:${nearMissWordAlternation(route)})[ \\t]*[)\\]}>]`, "iu");
|
|
13177
|
+
}
|
|
13178
|
+
function malformedAuthorizeAttemptPattern(route) {
|
|
13179
|
+
return new RegExp(`^##?\\|?[ \\t]*[([{<]?[ \\t]*(?:${nearMissWordAlternation(route)})`, "iu");
|
|
13180
|
+
}
|
|
13114
13181
|
function authorizeNoteContent(text) {
|
|
13115
|
-
|
|
13182
|
+
const route = canonicalAuthorizeRoute(noteRoute(text));
|
|
13183
|
+
if (route === undefined)
|
|
13184
|
+
return;
|
|
13185
|
+
return { route, content: notePayload(text) };
|
|
13186
|
+
}
|
|
13187
|
+
function authorizeAnnotationRoute(text) {
|
|
13188
|
+
return authorizeNoteContent(text)?.route;
|
|
13189
|
+
}
|
|
13190
|
+
function hasCallerAuthorizeAnnotation(callerText) {
|
|
13191
|
+
return AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText);
|
|
13116
13192
|
}
|
|
13117
13193
|
function assertNoCallerAuthorizeAnnotation(callerText) {
|
|
13118
13194
|
if (!AUTHORIZE_ANNOTATION_ANYWHERE.test(callerText))
|
|
@@ -13122,37 +13198,59 @@ function assertNoCallerAuthorizeAnnotation(callerText) {
|
|
|
13122
13198
|
function containsAuthorizeAnnotationTag(texts) {
|
|
13123
13199
|
return texts.some((text) => authorizeNoteContent(text) !== undefined);
|
|
13124
13200
|
}
|
|
13125
|
-
function collectAuthorizeNearMisses(texts) {
|
|
13201
|
+
function collectAuthorizeNearMisses(texts, route) {
|
|
13202
|
+
const motlyPattern = motlyAuthorizePayloadPattern(route);
|
|
13203
|
+
const malformedPattern = malformedAuthorizeAttemptPattern(route);
|
|
13204
|
+
const nearMissNames = new Set(nearMissRouteNames(route).map((name) => name.toLowerCase()));
|
|
13126
13205
|
const found = [];
|
|
13127
13206
|
for (const text of texts) {
|
|
13128
13207
|
const trimmed = text.trimStart();
|
|
13129
|
-
const
|
|
13130
|
-
if (
|
|
13208
|
+
const noted = noteRoute(trimmed);
|
|
13209
|
+
if (noted === route)
|
|
13131
13210
|
continue;
|
|
13132
|
-
const nearMiss =
|
|
13211
|
+
const nearMiss = noted === undefined ? malformedPattern.test(trimmed) : noted === "" ? motlyPattern.test(notePayload(trimmed)) : nearMissNames.has(noted.toLowerCase());
|
|
13133
13212
|
if (!nearMiss)
|
|
13134
13213
|
continue;
|
|
13135
13214
|
found.push(trimmed.split(/[\r\n]/, 1)[0]);
|
|
13136
13215
|
}
|
|
13137
13216
|
return found;
|
|
13138
13217
|
}
|
|
13218
|
+
function collectAuthorizeNearMissesAllRoutes(texts) {
|
|
13219
|
+
const materialized = [...texts];
|
|
13220
|
+
return RECOGNIZED_AUTHORIZE_SPELLINGS.flatMap((route) => collectAuthorizeNearMisses(materialized, route));
|
|
13221
|
+
}
|
|
13222
|
+
function guessedNearMissRoute(text) {
|
|
13223
|
+
const lower = text.toLowerCase();
|
|
13224
|
+
let best = ACCESS_FILTER_ROUTE;
|
|
13225
|
+
let bestLength = 0;
|
|
13226
|
+
for (const route of RECOGNIZED_AUTHORIZE_SPELLINGS) {
|
|
13227
|
+
for (const name of nearMissRouteNames(route)) {
|
|
13228
|
+
if (lower.includes(name) && name.length > bestLength) {
|
|
13229
|
+
best = route;
|
|
13230
|
+
bestLength = name.length;
|
|
13231
|
+
}
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
return best;
|
|
13235
|
+
}
|
|
13139
13236
|
function assertNoAuthorizeNearMisses(found) {
|
|
13140
13237
|
if (found.length === 0)
|
|
13141
13238
|
return;
|
|
13142
13239
|
const unique = [...new Set(found)];
|
|
13143
13240
|
throw new ModelCompilationError({
|
|
13144
|
-
message: `These annotations are
|
|
13145
|
-
${unique.map((t) => ` - \`${t}
|
|
13241
|
+
message: `These annotations are never enforced:
|
|
13242
|
+
${unique.map((t) => ` - \`${t}\` (meant \`#(${guessedNearMissRoute(t)})\`?)`).join(`
|
|
13146
13243
|
`)}
|
|
13147
|
-
` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\`
|
|
13244
|
+
` + `Malloy routes an annotation by its prefix, and only ` + `\`#(authorize)\` and \`#(access_filter)\` reach an access-control ` + `route (in their \`##\` and ` + `block-form \`#|(...)\` spellings too). A space after the \`#\`, spaces ` + `inside the brackets, a mis-hyphenated or transposed word, or anything ` + `trailing the closing bracket makes it a plain tag Malloy hands to ` + `something else. Write the tag named above, unquoted, on its own line ` + `directly above the \`source:\` statement you mean to protect (the ` + `quoted string form is retired). 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.`
|
|
13148
13245
|
});
|
|
13149
13246
|
}
|
|
13150
13247
|
function describeMisplacedAuthorizeAnnotation(f) {
|
|
13151
|
-
if (f.kind === "query")
|
|
13152
|
-
return `on query "${f.name}"`;
|
|
13153
13248
|
if (f.kind === "file")
|
|
13154
|
-
return
|
|
13155
|
-
|
|
13249
|
+
return `at the file level (\`##(${f.route})\`)`;
|
|
13250
|
+
const tag = `\`#(${f.route})\``;
|
|
13251
|
+
if (f.kind === "query")
|
|
13252
|
+
return `on query "${f.name}" (${tag})`;
|
|
13253
|
+
return `on field "${f.fieldName}" of source "${f.name}" (${tag})`;
|
|
13156
13254
|
}
|
|
13157
13255
|
function assertNoMisplacedAuthorizeAnnotations(found) {
|
|
13158
13256
|
if (found.length === 0)
|
|
@@ -13160,9 +13258,9 @@ function assertNoMisplacedAuthorizeAnnotations(found) {
|
|
|
13160
13258
|
const positions = found.map((f) => ` - ${describeMisplacedAuthorizeAnnotation(f)}`).join(`
|
|
13161
13259
|
`);
|
|
13162
13260
|
throw new ModelCompilationError({
|
|
13163
|
-
message: `An
|
|
13261
|
+
message: `An authorize annotation is never enforced at:
|
|
13164
13262
|
${positions}
|
|
13165
|
-
` + `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.
|
|
13263
|
+
` + `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. A file-level annotation (\`##(authorize)\` / ` + `\`##(access_filter)\`) is deprecated and no longer enforced ` + `anywhere, so it always lands here: declare the same tag on each ` + `\`source:\` it was meant to protect instead. Every other ` + `position above should move — with the same tag named beside it — ` + `to the \`source:\` statement it is meant ` + `to protect.`
|
|
13166
13264
|
});
|
|
13167
13265
|
}
|
|
13168
13266
|
function referencedGivenNames(expr) {
|
|
@@ -13199,7 +13297,7 @@ function liftProbeFilterCondition(prepared, label, filterText) {
|
|
|
13199
13297
|
return lifted;
|
|
13200
13298
|
}
|
|
13201
13299
|
function gateFilterText(exprs) {
|
|
13202
|
-
return exprs.map((e) => `(${e})`).join("
|
|
13300
|
+
return exprs.map((e) => `(${e})`).join(" and ");
|
|
13203
13301
|
}
|
|
13204
13302
|
async function liftRowLevelCondition(compiler, sourceName, exprs) {
|
|
13205
13303
|
const filterText = gateFilterText(exprs);
|
|
@@ -13209,7 +13307,7 @@ async function liftRowLevelCondition(compiler, sourceName, exprs) {
|
|
|
13209
13307
|
async function validateAuthorizeProbes(compiler, options) {
|
|
13210
13308
|
const ownNotesOf = options.authorizeOwnNotes ?? new Map;
|
|
13211
13309
|
for (const [sourceName, groups] of options.authorizeMap ?? []) {
|
|
13212
|
-
for (const exprs of groups) {
|
|
13310
|
+
for (const { route, exprs } of groups) {
|
|
13213
13311
|
if (exprs.length === 0)
|
|
13214
13312
|
continue;
|
|
13215
13313
|
let condition;
|
|
@@ -13217,17 +13315,17 @@ async function validateAuthorizeProbes(compiler, options) {
|
|
|
13217
13315
|
condition = await liftRowLevelCondition(compiler, sourceName, exprs);
|
|
13218
13316
|
} catch (err) {
|
|
13219
13317
|
const detail = err instanceof Error ? err.message : String(err);
|
|
13220
|
-
const ownNotes = ownNotesOf.get(sourceName) ?? [];
|
|
13318
|
+
const ownNotes = ownNotesOf.get(sourceName)?.get(route) ?? [];
|
|
13221
13319
|
if (ownNotes.length === 0) {
|
|
13222
13320
|
options.onRowLevelGateRejected?.("entry_point_unexpressible");
|
|
13223
13321
|
options.onRowLevelGateUnexpressible?.(sourceName, detail);
|
|
13224
13322
|
continue;
|
|
13225
13323
|
}
|
|
13226
13324
|
throw new ModelCompilationError({
|
|
13227
|
-
message: `Invalid #(
|
|
13325
|
+
message: `Invalid #(${route}) annotation on source "${sourceName}" ` + `[${exprs.join(" and ")}]: ${detail}`
|
|
13228
13326
|
});
|
|
13229
13327
|
}
|
|
13230
|
-
options.onOwnRowLevelConditionCompiled?.(sourceName, condition);
|
|
13328
|
+
options.onOwnRowLevelConditionCompiled?.(sourceName, condition, route);
|
|
13231
13329
|
}
|
|
13232
13330
|
}
|
|
13233
13331
|
}
|
|
@@ -13241,59 +13339,41 @@ function unquoteLegacyGatePayload(payload) {
|
|
|
13241
13339
|
return trimmed[0] === "'" ? inner.replace(SINGLE_QUOTE_ESCAPE, "$1") : inner.replace(DOUBLE_QUOTE_ESCAPE, "$1");
|
|
13242
13340
|
}
|
|
13243
13341
|
function parseAuthorizeAnnotation(annotation) {
|
|
13244
|
-
const
|
|
13245
|
-
if (
|
|
13342
|
+
const parsed = authorizeNoteContent(annotation);
|
|
13343
|
+
if (parsed === undefined)
|
|
13246
13344
|
return null;
|
|
13247
|
-
const trimmed = content.trim();
|
|
13345
|
+
const trimmed = parsed.content.trim();
|
|
13248
13346
|
if (trimmed.length === 0) {
|
|
13249
13347
|
throw new Error("authorize annotation has an empty expression body");
|
|
13250
13348
|
}
|
|
13251
|
-
return trimmed;
|
|
13349
|
+
return { route: parsed.route, expr: trimmed };
|
|
13252
13350
|
}
|
|
13253
13351
|
function collectAuthorizeExprs(annotations) {
|
|
13254
13352
|
const exprs = [];
|
|
13255
13353
|
for (const annotation of annotations) {
|
|
13256
|
-
const
|
|
13257
|
-
if (
|
|
13258
|
-
exprs.push(
|
|
13354
|
+
const parsed = parseAuthorizeAnnotation(annotation);
|
|
13355
|
+
if (parsed !== null) {
|
|
13356
|
+
exprs.push(parsed);
|
|
13259
13357
|
}
|
|
13260
13358
|
}
|
|
13261
13359
|
return exprs;
|
|
13262
13360
|
}
|
|
13263
|
-
function
|
|
13264
|
-
|
|
13265
|
-
for (const [sourceName, notes] of authorizeOwnNotes) {
|
|
13266
|
-
if (notes.length > 1) {
|
|
13267
|
-
found.push({ sourceName, texts: notes.map((note) => note.text) });
|
|
13268
|
-
}
|
|
13269
|
-
}
|
|
13270
|
-
return found;
|
|
13271
|
-
}
|
|
13272
|
-
function assertAtMostOneAuthorizeGate(found) {
|
|
13273
|
-
if (found.length === 0)
|
|
13274
|
-
return;
|
|
13275
|
-
const positions = found.map(({ sourceName, texts }) => ` - source "${sourceName}" declares ${texts.length}:
|
|
13276
|
-
` + texts.map((t) => ` \`${t.trim().split(/[\r\n]/, 1)[0]}\``).join(`
|
|
13277
|
-
`)).join(`
|
|
13278
|
-
`);
|
|
13279
|
-
throw new ModelCompilationError({
|
|
13280
|
-
message: `A source may declare at most one \`#(authorize)\` annotation:
|
|
13281
|
-
${positions}
|
|
13282
|
-
` + `Combine multiple conditions into one expression with \`or\` instead of ` + `repeating the annotation, e.g. ` + "`#(authorize) $ROLE = 'admin' or org_id in $GROUPS`."
|
|
13283
|
-
});
|
|
13361
|
+
function collectAuthorizeExprsForRoute(annotations, route) {
|
|
13362
|
+
return collectAuthorizeExprs(annotations).filter((e) => e.route === route).map((e) => e.expr);
|
|
13284
13363
|
}
|
|
13285
13364
|
function findLegacyStringGates(authorizeOwnNotes) {
|
|
13286
13365
|
const found = [];
|
|
13287
|
-
for (const [sourceName,
|
|
13366
|
+
for (const [sourceName, notesByRoute] of authorizeOwnNotes) {
|
|
13367
|
+
const notes = [...notesByRoute.values()].flat();
|
|
13288
13368
|
const legacyNotes = notes.filter((note) => {
|
|
13289
13369
|
const content = authorizeNoteContent(note.text);
|
|
13290
|
-
return content !== undefined && isLegacyQuotedPayload(content);
|
|
13370
|
+
return content !== undefined && isLegacyQuotedPayload(content.content);
|
|
13291
13371
|
});
|
|
13292
13372
|
if (legacyNotes.length === 0)
|
|
13293
13373
|
continue;
|
|
13294
|
-
const
|
|
13295
|
-
if (
|
|
13296
|
-
found.push({ sourceName,
|
|
13374
|
+
const gates = legacyNotes.flatMap((note) => collectAuthorizeExprs([note.text]));
|
|
13375
|
+
if (gates.length > 0) {
|
|
13376
|
+
found.push({ sourceName, gates });
|
|
13297
13377
|
}
|
|
13298
13378
|
}
|
|
13299
13379
|
return found;
|
|
@@ -13301,22 +13381,22 @@ function findLegacyStringGates(authorizeOwnNotes) {
|
|
|
13301
13381
|
function assertNoLegacyStringGate(found) {
|
|
13302
13382
|
if (found.length === 0)
|
|
13303
13383
|
return;
|
|
13304
|
-
const rewrites = found.flatMap(({ sourceName,
|
|
13305
|
-
#(
|
|
13384
|
+
const rewrites = found.flatMap(({ sourceName, gates }) => gates.map(({ route, expr }) => ` - source "${sourceName}": replace \`#(${route}) ${expr}\`` + ` with
|
|
13385
|
+
#(${route}) ${unquoteLegacyGatePayload(expr)}`)).join(`
|
|
13306
13386
|
`);
|
|
13307
13387
|
throw new ModelCompilationError({
|
|
13308
|
-
message: `The string form of
|
|
13388
|
+
message: `The string form of an authorize gate (a Malloy-quoted expression on ` + `the \`source:\` line) is no longer accepted. Replace it with the ` + `unquoted expression, carried by the same annotation on its own line ` + `directly above the \`source:\` line:
|
|
13309
13389
|
${rewrites}
|
|
13310
13390
|
` + `Test that the rewrite gates the same rows and check the row count ` + `matches what the string form served.`
|
|
13311
13391
|
});
|
|
13312
13392
|
}
|
|
13313
|
-
var
|
|
13393
|
+
var AUTHORIZE_TAG_LIKE, AUTHORIZE_ANNOTATION_ANYWHERE, REGEXP_SPECIAL, GIVEN_REF_PATTERN, SINGLE_QUOTE_ESCAPE, DOUBLE_QUOTE_ESCAPE, STRING_LITERAL_PATTERN, everyMemberOf = () => (...members) => members, ROW_LEVEL_GATE_REJECTION_CAUSES, WHOLE_BODY_QUOTED_STRING, WHOLE_BODY_SINGLE_QUOTED_STRING;
|
|
13314
13394
|
var init_authorize = __esm(() => {
|
|
13315
13395
|
init_errors();
|
|
13316
|
-
|
|
13396
|
+
init_authorize_routes();
|
|
13397
|
+
AUTHORIZE_TAG_LIKE = String.raw`##?\|?[ \t]*[([{<]?[ \t]*(?:(?:(?:row|source)[-_]?)?authorize|access[-_]?filter)(?=[)\]}>]|[ \t]|$)`;
|
|
13317
13398
|
AUTHORIZE_ANNOTATION_ANYWHERE = new RegExp(AUTHORIZE_TAG_LIKE, "iu");
|
|
13318
|
-
|
|
13319
|
-
MALFORMED_AUTHORIZE_ATTEMPT = /^##?\|?[ \t]*[([{<]?[ \t]*authorize/iu;
|
|
13399
|
+
REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g;
|
|
13320
13400
|
GIVEN_REF_PATTERN = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
13321
13401
|
SINGLE_QUOTE_ESCAPE = /\\(['\\])/g;
|
|
13322
13402
|
DOUBLE_QUOTE_ESCAPE = /\\(["\\])/g;
|
|
@@ -13341,20 +13421,33 @@ function recordAuthorizeBypass(entryPoint) {
|
|
|
13341
13421
|
}
|
|
13342
13422
|
function recordRowLevelGateDecision(decision) {
|
|
13343
13423
|
rowLevelDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_total", {
|
|
13344
|
-
description: "How a row-level `#(
|
|
13424
|
+
description: "How a row-level `#(access_filter)` 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."
|
|
13345
13425
|
});
|
|
13346
13426
|
rowLevelDecisionCounter.add(1, { decision });
|
|
13347
13427
|
}
|
|
13348
13428
|
function recordRowLevelGateRejected(cause) {
|
|
13349
13429
|
rowLevelRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_row_level_rejected_total", {
|
|
13350
|
-
description: "Row-level `#(
|
|
13430
|
+
description: "Row-level `#(access_filter)` gates that were refused, warned about, or could not be resolved. Label: cause (" + ROW_LEVEL_GATE_REJECTION_CAUSES.map((c) => `'${c}'`).join("|") + "). Only 'legacy_string_gate' fails the whole model load. 'entry_point_unexpressible', 'source_line_gate_no_given_reference' and 'source_line_gate_negated_membership' warn at load and leave the model servable. 'unreachable_given', 'given_usage_unresolvable' and 'unclassifiable_condition' are request-time only — that entry point denies every request. Alert on any nonzero value since the last publish; the request-time causes also make this a rate signal, so see the doc above before writing an alert."
|
|
13351
13431
|
});
|
|
13352
13432
|
rowLevelRejectionCounter.add(1, { cause });
|
|
13353
13433
|
}
|
|
13354
|
-
|
|
13434
|
+
function recordAuthorizeAdmitAllGate(route) {
|
|
13435
|
+
admitAllCounter ??= publisherMeter().createCounter("publisher_authorize_admit_all_total", {
|
|
13436
|
+
description: "Sources declaring their OWN unconditional admit-all gate (`#(authorize) true`), counted once each at package load. Label: route (" + CANONICAL_AUTHORIZE_ROUTES.map((r) => `'${r}'`).join("|") + "). Not an error — `true` is the only spelling for an extension that deliberately re-opens a gated base — but it is the one declaration that turns a gate off, so a jump since the last publish is worth a look."
|
|
13437
|
+
});
|
|
13438
|
+
admitAllCounter.add(1, { route });
|
|
13439
|
+
}
|
|
13440
|
+
function recordLockDecision(decision) {
|
|
13441
|
+
lockDecisionCounter ??= publisherMeter().createCounter("publisher_authorize_lock_total", {
|
|
13442
|
+
description: "How a `#(authorize)` lock resolved a request. Label: decision ('admitted'|'denied_by_lock'|'denied_unresolvable'). Both denials are a 403; 'denied_by_lock' is the gate refusing a caller it does not admit (routine), 'denied_unresolvable' is the fail-closed refusal when the gate could not be decided at all."
|
|
13443
|
+
});
|
|
13444
|
+
lockDecisionCounter.add(1, { decision });
|
|
13445
|
+
}
|
|
13446
|
+
var guardRejectionCounter = null, bypassCounter = null, rowLevelDecisionCounter = null, rowLevelRejectionCounter = null, admitAllCounter = null, lockDecisionCounter = null;
|
|
13355
13447
|
var init_authorize_metrics = __esm(() => {
|
|
13356
13448
|
init_telemetry();
|
|
13357
13449
|
init_authorize();
|
|
13450
|
+
init_authorize_routes();
|
|
13358
13451
|
});
|
|
13359
13452
|
|
|
13360
13453
|
// src/data_styles.ts
|
|
@@ -13480,6 +13573,317 @@ function ownLevelNotes(annote) {
|
|
|
13480
13573
|
}
|
|
13481
13574
|
var init_annotations = () => {};
|
|
13482
13575
|
|
|
13576
|
+
// src/service/authorize_grammar.ts
|
|
13577
|
+
import { routeOf as routeOf2 } from "@malloydata/malloy";
|
|
13578
|
+
function noteRoute2(text) {
|
|
13579
|
+
return routeOf2({ value: text.trimStart() });
|
|
13580
|
+
}
|
|
13581
|
+
function splitFieldPathSegments(fieldPath) {
|
|
13582
|
+
const segments = [];
|
|
13583
|
+
let i = 0;
|
|
13584
|
+
while (i < fieldPath.length) {
|
|
13585
|
+
if (fieldPath[i] === "`") {
|
|
13586
|
+
const end = fieldPath.indexOf("`", i + 1);
|
|
13587
|
+
segments.push(fieldPath.slice(i + 1, end));
|
|
13588
|
+
i = end + 1;
|
|
13589
|
+
} else {
|
|
13590
|
+
const dot = fieldPath.indexOf(".", i);
|
|
13591
|
+
const end = dot === -1 ? fieldPath.length : dot;
|
|
13592
|
+
segments.push(fieldPath.slice(i, end));
|
|
13593
|
+
i = end;
|
|
13594
|
+
}
|
|
13595
|
+
if (fieldPath[i] === ".")
|
|
13596
|
+
i++;
|
|
13597
|
+
}
|
|
13598
|
+
return segments;
|
|
13599
|
+
}
|
|
13600
|
+
function rejectionMessage(sourceName, body, detail) {
|
|
13601
|
+
return `Source "${sourceName}" declares an authorize gate \`${body}\`: ${detail} ` + GRAMMAR_SUMMARY;
|
|
13602
|
+
}
|
|
13603
|
+
function reject(sourceName, body, cause, detail) {
|
|
13604
|
+
throw new AuthorizeGrammarError(cause, rejectionMessage(sourceName, body, detail));
|
|
13605
|
+
}
|
|
13606
|
+
function rejectCoherence(sourceName, cause, detail) {
|
|
13607
|
+
throw new AuthorizeGrammarError(cause, `Source "${sourceName}" declares authorize notes whose terms ` + `conflict: ${detail} ${GRAMMAR_SUMMARY}`);
|
|
13608
|
+
}
|
|
13609
|
+
function splitTerms(body) {
|
|
13610
|
+
const terms = [];
|
|
13611
|
+
const compoundTokens = [];
|
|
13612
|
+
let depth = 0;
|
|
13613
|
+
let quote;
|
|
13614
|
+
let start = 0;
|
|
13615
|
+
let i = 0;
|
|
13616
|
+
while (i < body.length) {
|
|
13617
|
+
const ch = body[i];
|
|
13618
|
+
if (quote) {
|
|
13619
|
+
if (ch === "\\" && quote !== "`") {
|
|
13620
|
+
i += 2;
|
|
13621
|
+
continue;
|
|
13622
|
+
}
|
|
13623
|
+
if (ch === quote)
|
|
13624
|
+
quote = undefined;
|
|
13625
|
+
i++;
|
|
13626
|
+
continue;
|
|
13627
|
+
}
|
|
13628
|
+
if (isQuote(ch)) {
|
|
13629
|
+
quote = ch;
|
|
13630
|
+
i++;
|
|
13631
|
+
continue;
|
|
13632
|
+
}
|
|
13633
|
+
if (ch === "(") {
|
|
13634
|
+
depth++;
|
|
13635
|
+
i++;
|
|
13636
|
+
continue;
|
|
13637
|
+
}
|
|
13638
|
+
if (ch === ")") {
|
|
13639
|
+
depth = Math.max(0, depth - 1);
|
|
13640
|
+
i++;
|
|
13641
|
+
continue;
|
|
13642
|
+
}
|
|
13643
|
+
if (depth === 0) {
|
|
13644
|
+
const wordMatch = /^(and|or|not)\b/i.exec(body.slice(i));
|
|
13645
|
+
if (wordMatch && (i === 0 || !/[A-Za-z0-9_]/.test(body[i - 1]))) {
|
|
13646
|
+
const word = wordMatch[1].toLowerCase();
|
|
13647
|
+
if (word === "and") {
|
|
13648
|
+
terms.push(body.slice(start, i));
|
|
13649
|
+
i += wordMatch[0].length;
|
|
13650
|
+
start = i;
|
|
13651
|
+
continue;
|
|
13652
|
+
}
|
|
13653
|
+
compoundTokens.push(word);
|
|
13654
|
+
}
|
|
13655
|
+
}
|
|
13656
|
+
i++;
|
|
13657
|
+
}
|
|
13658
|
+
terms.push(body.slice(start));
|
|
13659
|
+
return { terms, compoundTokens };
|
|
13660
|
+
}
|
|
13661
|
+
function isQuote(ch) {
|
|
13662
|
+
return ch === "'" || ch === '"' || ch === "`";
|
|
13663
|
+
}
|
|
13664
|
+
function findOperatorOutsideQuotes(s, re) {
|
|
13665
|
+
let quote;
|
|
13666
|
+
for (let i = 0;i < s.length; i++) {
|
|
13667
|
+
const ch = s[i];
|
|
13668
|
+
if (quote) {
|
|
13669
|
+
if (ch === "\\" && quote !== "`") {
|
|
13670
|
+
i++;
|
|
13671
|
+
continue;
|
|
13672
|
+
}
|
|
13673
|
+
if (ch === quote)
|
|
13674
|
+
quote = undefined;
|
|
13675
|
+
continue;
|
|
13676
|
+
}
|
|
13677
|
+
if (isQuote(ch)) {
|
|
13678
|
+
quote = ch;
|
|
13679
|
+
continue;
|
|
13680
|
+
}
|
|
13681
|
+
re.lastIndex = i;
|
|
13682
|
+
const m = re.exec(s);
|
|
13683
|
+
if (m && m.index === i)
|
|
13684
|
+
return i;
|
|
13685
|
+
}
|
|
13686
|
+
return -1;
|
|
13687
|
+
}
|
|
13688
|
+
function parseTerm(sourceName, body, term, givenDeclaredTypes) {
|
|
13689
|
+
const trimmed = term.trim();
|
|
13690
|
+
if (findOperatorOutsideQuotes(trimmed, NEGATED_OPERATOR_RE) !== -1) {
|
|
13691
|
+
reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
|
|
13692
|
+
}
|
|
13693
|
+
if (findOperatorOutsideQuotes(trimmed, COMPARISON_OPERATOR_RE) !== -1) {
|
|
13694
|
+
reject(sourceName, body, "comparison_operator", "only `=` and `in` are allowed, not `<`/`>`/`<=`/`>=`.");
|
|
13695
|
+
}
|
|
13696
|
+
const inIdx = findOperatorOutsideQuotes(trimmed, IN_OPERATOR_RE);
|
|
13697
|
+
const eqIdx = findOperatorOutsideQuotes(trimmed, EQ_OPERATOR_RE);
|
|
13698
|
+
let operator;
|
|
13699
|
+
let opIdx;
|
|
13700
|
+
let opLen;
|
|
13701
|
+
if (inIdx !== -1 && (eqIdx === -1 || inIdx < eqIdx)) {
|
|
13702
|
+
operator = "in";
|
|
13703
|
+
opIdx = inIdx;
|
|
13704
|
+
opLen = 2;
|
|
13705
|
+
} else if (eqIdx !== -1) {
|
|
13706
|
+
operator = "=";
|
|
13707
|
+
opIdx = eqIdx;
|
|
13708
|
+
opLen = 1;
|
|
13709
|
+
} else {
|
|
13710
|
+
reject(sourceName, body, "malformed_body", "no `=` or `in` operator was found.");
|
|
13711
|
+
}
|
|
13712
|
+
let left = trimmed.slice(0, opIdx).trim();
|
|
13713
|
+
let right = trimmed.slice(opIdx + opLen).trim();
|
|
13714
|
+
if (GIVEN_REF_RE.test(left) && STRING_LITERAL_RE.test(right)) {
|
|
13715
|
+
if (operator === "in") {
|
|
13716
|
+
reject(sourceName, body, "reversed_in_operands", `\`${left} in ${right}\` reads $GIVEN in 'literal', which Malloy ` + `rejects as array-in-string membership. Write the literal ` + `first: \`${right} in ${left}\`.`);
|
|
13717
|
+
}
|
|
13718
|
+
[left, right] = [right, left];
|
|
13719
|
+
}
|
|
13720
|
+
const leftIsFieldPath = FIELD_PATH_RE.test(left);
|
|
13721
|
+
const leftIsLiteral = STRING_LITERAL_RE.test(left);
|
|
13722
|
+
if (!leftIsFieldPath && !leftIsLiteral) {
|
|
13723
|
+
reject(sourceName, body, "left_not_field_path", `\`${left}\` is neither a field path nor a string literal — one ` + "side must be a single column, a dotted join path, or a quoted " + "literal, and the other a `$GIVEN`.");
|
|
13724
|
+
}
|
|
13725
|
+
const givenMatch = GIVEN_REF_RE.exec(right);
|
|
13726
|
+
if (!givenMatch) {
|
|
13727
|
+
reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — a term compares against ` + "`$NAME`.");
|
|
13728
|
+
}
|
|
13729
|
+
const given = givenMatch[1];
|
|
13730
|
+
const declaredType = givenDeclaredTypes.get(given);
|
|
13731
|
+
if (declaredType !== undefined) {
|
|
13732
|
+
const isListType = declaredType === "array" || declaredType.endsWith("[]");
|
|
13733
|
+
if (operator === "=" && isListType) {
|
|
13734
|
+
reject(sourceName, body, "operator_arity_mismatch", `\`$${given}\` is a list-typed given — use \`in\`, not \`=\`.`);
|
|
13735
|
+
}
|
|
13736
|
+
if (operator === "in" && !isListType) {
|
|
13737
|
+
reject(sourceName, body, "operator_arity_mismatch", `\`$${given}\` is a scalar given — use \`=\`, not \`in\`.`);
|
|
13738
|
+
}
|
|
13739
|
+
}
|
|
13740
|
+
if (leftIsLiteral) {
|
|
13741
|
+
return { scope: "source_level", literal: left, given };
|
|
13742
|
+
}
|
|
13743
|
+
return {
|
|
13744
|
+
scope: "row_level",
|
|
13745
|
+
fieldPath: left,
|
|
13746
|
+
fieldPathSegments: splitFieldPathSegments(left),
|
|
13747
|
+
given,
|
|
13748
|
+
operator
|
|
13749
|
+
};
|
|
13750
|
+
}
|
|
13751
|
+
function assertAuthorizeGrammarTermsCoherent(sourceName, terms) {
|
|
13752
|
+
if (terms.length > 1 && terms.some(({ term }) => term.scope === "deny_all")) {
|
|
13753
|
+
rejectCoherence(sourceName, "deny_all_with_sibling", "an unconditional `false` deny-all cannot be combined with any " + "other authorize note — a deny-all admits nothing, so a " + "sibling note can never change what is served and its " + "presence is very likely a mistake.");
|
|
13754
|
+
}
|
|
13755
|
+
const byRoute = new Map;
|
|
13756
|
+
for (const { term, route } of terms) {
|
|
13757
|
+
if (term.scope === "deny_all")
|
|
13758
|
+
continue;
|
|
13759
|
+
const list = byRoute.get(route);
|
|
13760
|
+
if (list)
|
|
13761
|
+
list.push(term);
|
|
13762
|
+
else
|
|
13763
|
+
byRoute.set(route, [term]);
|
|
13764
|
+
}
|
|
13765
|
+
for (const allRouteTerms of byRoute.values()) {
|
|
13766
|
+
if (allRouteTerms.length > 1 && allRouteTerms.some((t) => t.scope === "admit_all")) {
|
|
13767
|
+
rejectCoherence(sourceName, "admit_all_with_sibling", "an unconditional `true` admit-all cannot be combined with " + "another `#(authorize)` note — the lock's notes AND " + "together, so `true and x` reduces to `x` and the admit-all " + "is dead text. To unlock a base while its rows stay " + "filtered, pair the `true` with `#(access_filter)` instead.");
|
|
13768
|
+
}
|
|
13769
|
+
const routeTerms = allRouteTerms.filter((t) => t.scope !== "admit_all" && t.scope !== "deny_all");
|
|
13770
|
+
const seenGivens = new Set;
|
|
13771
|
+
const seenFieldPaths = new Set;
|
|
13772
|
+
for (const t of routeTerms) {
|
|
13773
|
+
if (seenGivens.has(t.given)) {
|
|
13774
|
+
rejectCoherence(sourceName, "duplicate_given", `\`$${t.given}\` is used by more than one term — a given ` + "can back at most one term per route.");
|
|
13775
|
+
}
|
|
13776
|
+
seenGivens.add(t.given);
|
|
13777
|
+
if (t.scope === "row_level") {
|
|
13778
|
+
const canonicalPath = JSON.stringify(t.fieldPathSegments);
|
|
13779
|
+
if (seenFieldPaths.has(canonicalPath)) {
|
|
13780
|
+
rejectCoherence(sourceName, "duplicate_field_path", `\`${t.fieldPath}\` is used by more than one term.`);
|
|
13781
|
+
}
|
|
13782
|
+
seenFieldPaths.add(canonicalPath);
|
|
13783
|
+
}
|
|
13784
|
+
}
|
|
13785
|
+
}
|
|
13786
|
+
}
|
|
13787
|
+
function parseAuthorizeGrammarBody(sourceName, body, givenDeclaredTypes, route) {
|
|
13788
|
+
const trimmedBody = body.trim();
|
|
13789
|
+
if (trimmedBody.length === 0) {
|
|
13790
|
+
reject(sourceName, trimmedBody, "empty_body", "the expression body is empty.");
|
|
13791
|
+
}
|
|
13792
|
+
const sentinel = trimmedBody.toLowerCase();
|
|
13793
|
+
if (sentinel === "false" || sentinel === "true") {
|
|
13794
|
+
if (route !== AUTHORIZE_ROUTE) {
|
|
13795
|
+
reject(sourceName, trimmedBody, "sentinel_in_access_filter", `a bare \`${sentinel}\` is not allowed in \`#(access_filter)\` — ` + (sentinel === "false" ? "write `#(authorize) false`, which refuses the caller " + "rather than serving them zero rows." : "it filters nothing; delete the annotation, or write " + "`#(authorize) true` to re-open a locked base."));
|
|
13796
|
+
}
|
|
13797
|
+
return [{ scope: sentinel === "false" ? "deny_all" : "admit_all" }];
|
|
13798
|
+
}
|
|
13799
|
+
const { terms: rawTerms, compoundTokens } = splitTerms(trimmedBody);
|
|
13800
|
+
if (compoundTokens.length > 0) {
|
|
13801
|
+
reject(sourceName, trimmedBody, "compound_boolean", `a compound boolean (\`${compoundTokens[0]}\`) is not allowed — ` + "join terms with `and` only.");
|
|
13802
|
+
}
|
|
13803
|
+
const parsed = rawTerms.map((term) => parseTerm(sourceName, trimmedBody, term, givenDeclaredTypes));
|
|
13804
|
+
const ADMITTED_SCOPE = {
|
|
13805
|
+
[AUTHORIZE_ROUTE]: "source_level",
|
|
13806
|
+
[ACCESS_FILTER_ROUTE]: "row_level"
|
|
13807
|
+
};
|
|
13808
|
+
const admitted = ADMITTED_SCOPE[route];
|
|
13809
|
+
const offending = parsed.find((t) => t.scope !== admitted);
|
|
13810
|
+
if (offending) {
|
|
13811
|
+
const inLock = route === AUTHORIZE_ROUTE;
|
|
13812
|
+
reject(sourceName, trimmedBody, inLock ? "row_level_term_in_authorize" : "source_level_term_in_access_filter", inLock ? "a row-level term (field on the left) is not allowed in " + "`#(authorize)`, which decides whether the caller may reach " + "the source at all — move the term to `#(access_filter)`." : "a source-level term (literal on the left) is not allowed in " + "`#(access_filter)`, which decides which rows a caller may " + "see — move the term to `#(authorize)`, which refuses a " + "caller it does not admit rather than serving them zero rows.");
|
|
13813
|
+
}
|
|
13814
|
+
assertAuthorizeGrammarTermsCoherent(sourceName, parsed.map((term) => ({ term, route })));
|
|
13815
|
+
return parsed;
|
|
13816
|
+
}
|
|
13817
|
+
function containsRetiredRouteTag(texts) {
|
|
13818
|
+
for (const text of texts) {
|
|
13819
|
+
const route = noteRoute2(text);
|
|
13820
|
+
if (route !== undefined && RETIRED_ROUTES.includes(route)) {
|
|
13821
|
+
return route;
|
|
13822
|
+
}
|
|
13823
|
+
}
|
|
13824
|
+
return;
|
|
13825
|
+
}
|
|
13826
|
+
function reachesRetiredRouteTagBelow(node, seen = new WeakSet, depth = 0) {
|
|
13827
|
+
if (depth > MAX_RETIRED_ROUTE_WALK_DEPTH) {
|
|
13828
|
+
throw new Error("retired-route IR walk exceeded max depth");
|
|
13829
|
+
}
|
|
13830
|
+
if (node === null || typeof node !== "object")
|
|
13831
|
+
return;
|
|
13832
|
+
if (seen.has(node))
|
|
13833
|
+
return;
|
|
13834
|
+
seen.add(node);
|
|
13835
|
+
if (Array.isArray(node)) {
|
|
13836
|
+
for (const item of node) {
|
|
13837
|
+
const found = reachesRetiredRouteTagBelow(item, seen, depth + 1);
|
|
13838
|
+
if (found)
|
|
13839
|
+
return found;
|
|
13840
|
+
}
|
|
13841
|
+
return;
|
|
13842
|
+
}
|
|
13843
|
+
const record = node;
|
|
13844
|
+
if (depth > 0) {
|
|
13845
|
+
for (const key of ["blockNotes", "notes"]) {
|
|
13846
|
+
const arr = record[key];
|
|
13847
|
+
if (!Array.isArray(arr))
|
|
13848
|
+
continue;
|
|
13849
|
+
const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
|
|
13850
|
+
const found = containsRetiredRouteTag(texts);
|
|
13851
|
+
if (found)
|
|
13852
|
+
return found;
|
|
13853
|
+
}
|
|
13854
|
+
}
|
|
13855
|
+
for (const [key, value] of Object.entries(record)) {
|
|
13856
|
+
if (depth === 0 && key === "annotations")
|
|
13857
|
+
continue;
|
|
13858
|
+
const found = reachesRetiredRouteTagBelow(value, seen, depth + 1);
|
|
13859
|
+
if (found)
|
|
13860
|
+
return found;
|
|
13861
|
+
}
|
|
13862
|
+
return;
|
|
13863
|
+
}
|
|
13864
|
+
var AuthorizeGrammarError, IDENT = "[A-Za-z_][A-Za-z0-9_]*", QUOTED_IDENT = "`[^`]+`", SEGMENT, FIELD_PATH_RE, GIVEN_REF_RE, STRING_LITERAL_RE, NEGATED_OPERATOR_RE, COMPARISON_OPERATOR_RE, GRAMMAR_SUMMARY, IN_OPERATOR_RE, EQ_OPERATOR_RE, RETIRED_ROUTES, MAX_RETIRED_ROUTE_WALK_DEPTH = 200;
|
|
13865
|
+
var init_authorize_grammar = __esm(() => {
|
|
13866
|
+
init_errors();
|
|
13867
|
+
init_authorize_routes();
|
|
13868
|
+
AuthorizeGrammarError = class AuthorizeGrammarError extends ModelCompilationError {
|
|
13869
|
+
rejectionCause;
|
|
13870
|
+
constructor(rejectionCause, message) {
|
|
13871
|
+
super({ message });
|
|
13872
|
+
this.rejectionCause = rejectionCause;
|
|
13873
|
+
}
|
|
13874
|
+
};
|
|
13875
|
+
SEGMENT = `(?:${IDENT}|${QUOTED_IDENT})`;
|
|
13876
|
+
FIELD_PATH_RE = new RegExp(`^${SEGMENT}(?:\\.${SEGMENT})*$`);
|
|
13877
|
+
GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
|
|
13878
|
+
STRING_LITERAL_RE = /^(?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")$/;
|
|
13879
|
+
NEGATED_OPERATOR_RE = /!=/gu;
|
|
13880
|
+
COMPARISON_OPERATOR_RE = /(?:>=|<=|>|<)/gu;
|
|
13881
|
+
GRAMMAR_SUMMARY = "An authorize gate only accepts one or more `and`-joined terms, each " + "either `<column> <op> $GIVEN` (a single field or a dotted join " + "path on the left) or `'<literal>' in $GIVEN` (a literal on the " + "left), where <op> is fixed by the given's own arity: `in` for a " + "list-typed given, `=` for a scalar one.";
|
|
13882
|
+
IN_OPERATOR_RE = /\bin\b/giu;
|
|
13883
|
+
EQ_OPERATOR_RE = /=/gu;
|
|
13884
|
+
RETIRED_ROUTES = ["partition"];
|
|
13885
|
+
});
|
|
13886
|
+
|
|
13483
13887
|
// src/service/gate_dimension.ts
|
|
13484
13888
|
import {
|
|
13485
13889
|
isJoined,
|
|
@@ -13544,11 +13948,12 @@ function containsNegatedMembership(node, depth = 0) {
|
|
|
13544
13948
|
return containsNegatedMembership(n.e, depth + 1);
|
|
13545
13949
|
return false;
|
|
13546
13950
|
}
|
|
13547
|
-
function validateSourceLineGateGivenUsage(sourceName, struct, refSummary, conditionExpr, modelDef, onWarning) {
|
|
13951
|
+
function validateSourceLineGateGivenUsage(sourceName, route, struct, refSummary, conditionExpr, modelDef, onWarning) {
|
|
13952
|
+
const gate = `#(${route}) gate`;
|
|
13548
13953
|
const expansion = expandRefSummaryGivenIds(struct, refSummary);
|
|
13549
13954
|
if (!expansion.ok) {
|
|
13550
13955
|
throw new ModelCompilationError({
|
|
13551
|
-
message:
|
|
13956
|
+
message: `The ${gate} on source "${sourceName}" references ` + `"${expansion.unresolvedPath}", which could not be resolved to a ` + `field this model can reach. An unresolvable reference is refused, ` + `not treated as referencing no given`
|
|
13552
13957
|
});
|
|
13553
13958
|
}
|
|
13554
13959
|
const givenIds = expansion.givenIds;
|
|
@@ -13556,15 +13961,15 @@ function validateSourceLineGateGivenUsage(sourceName, struct, refSummary, condit
|
|
|
13556
13961
|
const given = modelDef.givens?.[id];
|
|
13557
13962
|
if (given && (given.default !== undefined || given.defaultText !== undefined)) {
|
|
13558
13963
|
throw new ModelCompilationError({
|
|
13559
|
-
message:
|
|
13964
|
+
message: `The ${gate} on source "${sourceName}" references ` + `\`$${given.name}\`, which is declared with a default. A ` + `caller who supplies no value for \`$${given.name}\` gets ` + `that default, which can admit what the gate was meant to ` + `exclude. Declare \`$${given.name}\` with no default`
|
|
13560
13965
|
});
|
|
13561
13966
|
}
|
|
13562
13967
|
}
|
|
13563
13968
|
if (givenIds.size === 0) {
|
|
13564
|
-
onWarning?.("source_line_gate_no_given_reference",
|
|
13969
|
+
onWarning?.("source_line_gate_no_given_reference", `The ${gate} on source "${sourceName}" references no given; it is ` + `a fixed predicate, not an access rule keyed on the caller`);
|
|
13565
13970
|
}
|
|
13566
13971
|
if (containsNegatedMembership(conditionExpr)) {
|
|
13567
|
-
onWarning?.("source_line_gate_negated_membership",
|
|
13972
|
+
onWarning?.("source_line_gate_negated_membership", `The ${gate} on source "${sourceName}" negates a membership test; ` + `an empty given then matches everything instead of nothing`);
|
|
13568
13973
|
}
|
|
13569
13974
|
}
|
|
13570
13975
|
var init_gate_dimension = __esm(() => {
|
|
@@ -13592,10 +13997,10 @@ function resolveDeclaredSource(struct, modelDef) {
|
|
|
13592
13997
|
}
|
|
13593
13998
|
return sawBrokenEntry ? { kind: "unresolvable" } : { kind: "none" };
|
|
13594
13999
|
}
|
|
13595
|
-
function ancestorGateExprs(struct, modelDef, seen = new Set) {
|
|
14000
|
+
function ancestorGateExprs(struct, modelDef, route, seen = new Set) {
|
|
13596
14001
|
let inherited = struct.annotations?.inherits;
|
|
13597
14002
|
for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
|
|
13598
|
-
const exprs2 =
|
|
14003
|
+
const exprs2 = collectAuthorizeExprsForRoute(ownLevelNoteTexts(inherited), route);
|
|
13599
14004
|
if (exprs2.length > 0)
|
|
13600
14005
|
return exprs2;
|
|
13601
14006
|
inherited = inherited.inherits;
|
|
@@ -13603,15 +14008,17 @@ function ancestorGateExprs(struct, modelDef, seen = new Set) {
|
|
|
13603
14008
|
if (inherited)
|
|
13604
14009
|
return ["false"];
|
|
13605
14010
|
seen.add(struct);
|
|
13606
|
-
if (seen.size > ANCESTOR_WALK_MAX_DEPTH)
|
|
14011
|
+
if (seen.size > ANCESTOR_WALK_MAX_DEPTH) {
|
|
13607
14012
|
return ["false"];
|
|
14013
|
+
}
|
|
13608
14014
|
const declared = resolveDeclaredSource(struct, modelDef);
|
|
13609
|
-
if (declared.kind === "unresolvable")
|
|
14015
|
+
if (declared.kind === "unresolvable") {
|
|
13610
14016
|
return ["false"];
|
|
14017
|
+
}
|
|
13611
14018
|
if (declared.kind === "none" || seen.has(declared.source))
|
|
13612
14019
|
return [];
|
|
13613
|
-
const exprs =
|
|
13614
|
-
return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, seen);
|
|
14020
|
+
const exprs = collectAuthorizeExprsForRoute(ownLevelNoteTexts(declared.source.annotations), route);
|
|
14021
|
+
return exprs.length > 0 ? exprs : ancestorGateExprs(declared.source, modelDef, route, seen);
|
|
13615
14022
|
}
|
|
13616
14023
|
function resolveQuerySourceBase(struct, modelDef) {
|
|
13617
14024
|
const duck = struct;
|
|
@@ -13625,8 +14032,8 @@ function resolveCompositeResolvedBase(struct) {
|
|
|
13625
14032
|
const duck = struct;
|
|
13626
14033
|
return duck.type === "query_source" ? duck.query?.compositeResolvedSourceDef : undefined;
|
|
13627
14034
|
}
|
|
13628
|
-
function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
|
|
13629
|
-
const direct = ancestorGateExprs(struct, modelDef, new Set(seen));
|
|
14035
|
+
function effectiveAncestorGateExprs(struct, modelDef, route, seen = new Set) {
|
|
14036
|
+
const direct = ancestorGateExprs(struct, modelDef, route, new Set(seen));
|
|
13630
14037
|
if (direct.length > 0)
|
|
13631
14038
|
return [direct];
|
|
13632
14039
|
if (seen.has(struct))
|
|
@@ -13636,18 +14043,19 @@ function effectiveAncestorGateExprs(struct, modelDef, seen = new Set) {
|
|
|
13636
14043
|
const base = resolveQuerySourceBase(struct, modelDef);
|
|
13637
14044
|
if (!base) {
|
|
13638
14045
|
const duck = struct;
|
|
13639
|
-
if (duck.type === "query_source")
|
|
14046
|
+
if (duck.type === "query_source") {
|
|
13640
14047
|
groups.push(["false"]);
|
|
14048
|
+
}
|
|
13641
14049
|
} else if (!seen.has(base)) {
|
|
13642
|
-
const ownExprs =
|
|
13643
|
-
groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, seen));
|
|
14050
|
+
const ownExprs = collectAuthorizeExprsForRoute(ownLevelNoteTexts(base.annotations), route);
|
|
14051
|
+
groups.push(...ownExprs.length > 0 ? [ownExprs] : effectiveAncestorGateExprs(base, modelDef, route, seen));
|
|
13644
14052
|
}
|
|
13645
14053
|
const composite = resolveCompositeResolvedBase(struct);
|
|
13646
14054
|
if (composite && !seen.has(composite)) {
|
|
13647
14055
|
const parentOwnNotes = base ? ownLevelNotes(base.annotations) : [];
|
|
13648
14056
|
const compositeOwnNotes = ownLevelNotes(composite.annotations).filter((note) => !parentOwnNotes.includes(note));
|
|
13649
|
-
const compositeOwn =
|
|
13650
|
-
groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, seen));
|
|
14057
|
+
const compositeOwn = collectAuthorizeExprsForRoute(compositeOwnNotes.map((note) => note.text), route);
|
|
14058
|
+
groups.push(...compositeOwn.length > 0 ? [compositeOwn] : effectiveAncestorGateExprs(composite, modelDef, route, seen));
|
|
13651
14059
|
}
|
|
13652
14060
|
return groups;
|
|
13653
14061
|
}
|
|
@@ -13676,120 +14084,9 @@ var init_gate_registry_walk = __esm(() => {
|
|
|
13676
14084
|
init_authorize();
|
|
13677
14085
|
});
|
|
13678
14086
|
|
|
13679
|
-
// src/service/partition_annotation.ts
|
|
13680
|
-
import { payloadOf as payloadOf2, routeOf as routeOf2 } from "@malloydata/malloy";
|
|
13681
|
-
function noteRoute2(text) {
|
|
13682
|
-
return routeOf2({ value: text.trimStart() });
|
|
13683
|
-
}
|
|
13684
|
-
function containsPartitionAnnotationTag(texts) {
|
|
13685
|
-
return texts.some((text) => noteRoute2(text) === PARTITION_ROUTE);
|
|
13686
|
-
}
|
|
13687
|
-
function reachesPartitionTagBelow(node, seen = new WeakSet, depth = 0) {
|
|
13688
|
-
if (depth > MAX_PARTITION_IR_WALK_DEPTH) {
|
|
13689
|
-
throw new Error("partition-marker IR walk exceeded max depth");
|
|
13690
|
-
}
|
|
13691
|
-
if (node === null || typeof node !== "object")
|
|
13692
|
-
return false;
|
|
13693
|
-
if (seen.has(node))
|
|
13694
|
-
return false;
|
|
13695
|
-
seen.add(node);
|
|
13696
|
-
if (Array.isArray(node)) {
|
|
13697
|
-
return node.some((item) => reachesPartitionTagBelow(item, seen, depth + 1));
|
|
13698
|
-
}
|
|
13699
|
-
const record = node;
|
|
13700
|
-
if (depth > 0 && record.join !== undefined)
|
|
13701
|
-
return false;
|
|
13702
|
-
if (depth > 0) {
|
|
13703
|
-
for (const key of ["blockNotes", "notes"]) {
|
|
13704
|
-
const arr = record[key];
|
|
13705
|
-
if (!Array.isArray(arr))
|
|
13706
|
-
continue;
|
|
13707
|
-
const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
|
|
13708
|
-
if (containsPartitionAnnotationTag(texts))
|
|
13709
|
-
return true;
|
|
13710
|
-
}
|
|
13711
|
-
}
|
|
13712
|
-
return Object.entries(record).some(([key, value]) => depth === 0 && key === "annotations" ? false : reachesPartitionTagBelow(value, seen, depth + 1));
|
|
13713
|
-
}
|
|
13714
|
-
function notePayload2(text) {
|
|
13715
|
-
return payloadOf2({ value: text.trimStart() }) ?? "";
|
|
13716
|
-
}
|
|
13717
|
-
function rejectionMessage(sourceName, body, detail) {
|
|
13718
|
-
return `Source "${sourceName}" declares \`#(partition) ${body}\`: ${detail} ` + `#(partition) only accepts \`<column> = $GIVEN\`, where <column> is a ` + `single field or a dotted join path.`;
|
|
13719
|
-
}
|
|
13720
|
-
function reject(sourceName, body, cause, detail) {
|
|
13721
|
-
throw new PartitionAnnotationError(cause, rejectionMessage(sourceName, body, detail));
|
|
13722
|
-
}
|
|
13723
|
-
function parsePartitionAnnotation(sourceName, annotationText) {
|
|
13724
|
-
if (noteRoute2(annotationText) !== PARTITION_ROUTE)
|
|
13725
|
-
return null;
|
|
13726
|
-
const body = notePayload2(annotationText).trim();
|
|
13727
|
-
if (body.length === 0) {
|
|
13728
|
-
reject(sourceName, body, "empty_body", "the expression body is empty.");
|
|
13729
|
-
}
|
|
13730
|
-
if (COMPOUND_BOOLEAN_RE.test(body)) {
|
|
13731
|
-
reject(sourceName, body, "compound_boolean", "a compound boolean (`and`/`or`/`not`) is not allowed — declare one " + "`#(partition)` marker per column.");
|
|
13732
|
-
}
|
|
13733
|
-
if (IN_OPERATOR_RE.test(body)) {
|
|
13734
|
-
reject(sourceName, body, "in_operator", "the `in` operator is not allowed.");
|
|
13735
|
-
}
|
|
13736
|
-
if (NEGATED_OPERATOR_RE.test(body)) {
|
|
13737
|
-
reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
|
|
13738
|
-
}
|
|
13739
|
-
if (COMPARISON_OPERATOR_RE.test(body)) {
|
|
13740
|
-
reject(sourceName, body, "comparison_operator", "only `=` is allowed, not `<`/`>`/`<=`/`>=`.");
|
|
13741
|
-
}
|
|
13742
|
-
const eq = body.indexOf("=");
|
|
13743
|
-
if (eq === -1) {
|
|
13744
|
-
reject(sourceName, body, "malformed_body", "no `=` was found.");
|
|
13745
|
-
}
|
|
13746
|
-
const left = body.slice(0, eq).trim();
|
|
13747
|
-
const right = body.slice(eq + 1).trim();
|
|
13748
|
-
if (!FIELD_PATH_RE.test(left)) {
|
|
13749
|
-
reject(sourceName, body, "left_not_field_path", `\`${left}\` is not a field path — the left side must be a single ` + "column or a dotted join path, not an expression.");
|
|
13750
|
-
}
|
|
13751
|
-
const givenMatch = GIVEN_REF_RE.exec(right);
|
|
13752
|
-
if (!givenMatch) {
|
|
13753
|
-
reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — the right side must be ` + "`$NAME`.");
|
|
13754
|
-
}
|
|
13755
|
-
return { column: left, given: givenMatch[1] };
|
|
13756
|
-
}
|
|
13757
|
-
function collectPartitionPairs(sourceName, annotationTexts2) {
|
|
13758
|
-
const pairs = [];
|
|
13759
|
-
const seenGivens = new Map;
|
|
13760
|
-
for (const text of annotationTexts2) {
|
|
13761
|
-
const pair = parsePartitionAnnotation(sourceName, text);
|
|
13762
|
-
if (pair === null)
|
|
13763
|
-
continue;
|
|
13764
|
-
const priorColumn = seenGivens.get(pair.given);
|
|
13765
|
-
if (priorColumn !== undefined) {
|
|
13766
|
-
throw new PartitionAnnotationError("duplicate_given", `Source "${sourceName}" declares \`#(partition)\` on both ` + `\`${priorColumn}\` and \`${pair.column}\` for the same given ` + `\`$${pair.given}\` — a given can back at most one partition ` + `column per source.`);
|
|
13767
|
-
}
|
|
13768
|
-
seenGivens.set(pair.given, pair.column);
|
|
13769
|
-
pairs.push(pair);
|
|
13770
|
-
}
|
|
13771
|
-
return pairs;
|
|
13772
|
-
}
|
|
13773
|
-
var PARTITION_ROUTE = "partition", MAX_PARTITION_IR_WALK_DEPTH = 200, PartitionAnnotationError, IDENT = "[A-Za-z_][A-Za-z0-9_]*", FIELD_PATH_RE, GIVEN_REF_RE, COMPOUND_BOOLEAN_RE, IN_OPERATOR_RE, NEGATED_OPERATOR_RE, COMPARISON_OPERATOR_RE;
|
|
13774
|
-
var init_partition_annotation = __esm(() => {
|
|
13775
|
-
init_errors();
|
|
13776
|
-
PartitionAnnotationError = class PartitionAnnotationError extends ModelCompilationError {
|
|
13777
|
-
rejectionCause;
|
|
13778
|
-
constructor(rejectionCause, message) {
|
|
13779
|
-
super({ message });
|
|
13780
|
-
this.rejectionCause = rejectionCause;
|
|
13781
|
-
}
|
|
13782
|
-
};
|
|
13783
|
-
FIELD_PATH_RE = new RegExp(`^${IDENT}(?:\\.${IDENT})*$`);
|
|
13784
|
-
GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
|
|
13785
|
-
COMPOUND_BOOLEAN_RE = /\b(and|or|not)\b/i;
|
|
13786
|
-
IN_OPERATOR_RE = /\bin\b/i;
|
|
13787
|
-
NEGATED_OPERATOR_RE = /!=/;
|
|
13788
|
-
COMPARISON_OPERATOR_RE = /(>=|<=|>|<)/;
|
|
13789
|
-
});
|
|
13790
|
-
|
|
13791
14087
|
// src/service/gate_classification.ts
|
|
13792
14088
|
import {
|
|
14089
|
+
isJoined as isJoined2,
|
|
13793
14090
|
isSourceDef as isSourceDef3
|
|
13794
14091
|
} from "@malloydata/malloy";
|
|
13795
14092
|
function createGateClassificationDeps(givens, modelPath) {
|
|
@@ -13799,30 +14096,41 @@ function createGateClassificationDeps(givens, modelPath) {
|
|
|
13799
14096
|
modelPath
|
|
13800
14097
|
};
|
|
13801
14098
|
}
|
|
13802
|
-
function gateExprsForOwnAnnotations(struct, modelDef, excludeNotes = []) {
|
|
14099
|
+
function gateExprsForOwnAnnotations(struct, route, modelDef, excludeNotes = []) {
|
|
13803
14100
|
const ownNotes = ownLevelNotes(struct.annotations).filter((note) => !excludeNotes.includes(note));
|
|
13804
14101
|
try {
|
|
13805
|
-
const own =
|
|
14102
|
+
const own = collectAuthorizeExprsForRoute(ownNotes.map((note) => note.text), route);
|
|
13806
14103
|
if (own.length > 0) {
|
|
13807
14104
|
return { exprs: own, fromAncestor: false };
|
|
13808
14105
|
}
|
|
13809
|
-
const ancestor = ancestorGateExprs(struct, modelDef);
|
|
14106
|
+
const ancestor = ancestorGateExprs(struct, modelDef, route);
|
|
13810
14107
|
return { exprs: ancestor, fromAncestor: ancestor.length > 0 };
|
|
13811
14108
|
} catch {
|
|
13812
|
-
return {
|
|
14109
|
+
return {
|
|
14110
|
+
exprs: ["false"],
|
|
14111
|
+
fromAncestor: false
|
|
14112
|
+
};
|
|
13813
14113
|
}
|
|
13814
14114
|
}
|
|
13815
14115
|
function collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate = false, entryPointStruct = struct, excludeNotes = []) {
|
|
14116
|
+
const results = [];
|
|
14117
|
+
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
14118
|
+
results.push(...collectEntryPointGatesForRoute(struct, modelDef, route, new Set(seen), treatAsOwnGate, entryPointStruct, excludeNotes));
|
|
14119
|
+
}
|
|
14120
|
+
return results;
|
|
14121
|
+
}
|
|
14122
|
+
function collectEntryPointGatesForRoute(struct, modelDef, route, seen, treatAsOwnGate, entryPointStruct, excludeNotes) {
|
|
13816
14123
|
if (!struct || !modelDef || seen.has(struct))
|
|
13817
14124
|
return [];
|
|
13818
14125
|
seen.add(struct);
|
|
13819
14126
|
const results = [];
|
|
13820
14127
|
const label = struct.as ?? struct.name;
|
|
13821
|
-
const { exprs: ownExprs, fromAncestor } = gateExprsForOwnAnnotations(struct, modelDef, excludeNotes);
|
|
14128
|
+
const { exprs: ownExprs, fromAncestor } = gateExprsForOwnAnnotations(struct, route, modelDef, excludeNotes);
|
|
13822
14129
|
if (ownExprs.length > 0) {
|
|
13823
14130
|
results.push({
|
|
13824
14131
|
label,
|
|
13825
14132
|
exprs: ownExprs,
|
|
14133
|
+
route,
|
|
13826
14134
|
selfContained: fromAncestor || !treatAsOwnGate,
|
|
13827
14135
|
struct: entryPointStruct
|
|
13828
14136
|
});
|
|
@@ -13831,17 +14139,18 @@ function collectEntryPointGates(struct, modelDef, seen = new Set, treatAsOwnGate
|
|
|
13831
14139
|
if (duck.type === "query_source") {
|
|
13832
14140
|
const base = resolveQuerySourceBase(struct, modelDef);
|
|
13833
14141
|
if (base) {
|
|
13834
|
-
results.push(...
|
|
14142
|
+
results.push(...collectEntryPointGatesForRoute(base, modelDef, route, seen, false, entryPointStruct, []));
|
|
13835
14143
|
} else {
|
|
13836
14144
|
results.push({
|
|
13837
14145
|
label,
|
|
13838
14146
|
exprs: ["false"],
|
|
14147
|
+
route,
|
|
13839
14148
|
selfContained: true
|
|
13840
14149
|
});
|
|
13841
14150
|
}
|
|
13842
14151
|
const resolved = duck.query?.compositeResolvedSourceDef;
|
|
13843
14152
|
if (resolved) {
|
|
13844
|
-
results.push(...
|
|
14153
|
+
results.push(...collectEntryPointGatesForRoute(resolved, modelDef, route, seen, false, entryPointStruct, base ? ownLevelNotes(base.annotations) : []));
|
|
13845
14154
|
}
|
|
13846
14155
|
}
|
|
13847
14156
|
return results;
|
|
@@ -13858,7 +14167,7 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
|
|
|
13858
14167
|
return { shape: "rejected" };
|
|
13859
14168
|
}
|
|
13860
14169
|
const filterText = gateFilterText(entry.exprs);
|
|
13861
|
-
const cacheKey = `${graftScope.cacheScope}\x00${graftTarget}\x00${filterText}`;
|
|
14170
|
+
const cacheKey = `${graftScope.cacheScope}\x00${entry.route}\x00${graftTarget}\x00${filterText}`;
|
|
13862
14171
|
let cached = deps.gateShapeCache.get(cacheKey);
|
|
13863
14172
|
if (!cached) {
|
|
13864
14173
|
let condition;
|
|
@@ -13881,7 +14190,11 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
|
|
|
13881
14190
|
detail: "this entry's lifted condition carries no usable expression"
|
|
13882
14191
|
};
|
|
13883
14192
|
} else if (isBareFalseLiteral(condition.e)) {
|
|
13884
|
-
classification = {
|
|
14193
|
+
classification = {
|
|
14194
|
+
shape: "row_level",
|
|
14195
|
+
givenNames: [],
|
|
14196
|
+
givenNamesById: new Map
|
|
14197
|
+
};
|
|
13885
14198
|
} else {
|
|
13886
14199
|
const targetStruct = graftScope.modelDef.contents[graftTarget];
|
|
13887
14200
|
if (!isSourceDef3(targetStruct)) {
|
|
@@ -13899,7 +14212,13 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
|
|
|
13899
14212
|
detail: `this gate references \`${expansion.unresolvedPath}\`, which could not be resolved on the graft target`
|
|
13900
14213
|
};
|
|
13901
14214
|
} else {
|
|
13902
|
-
const
|
|
14215
|
+
const givenNamesById = new Map;
|
|
14216
|
+
for (const id of expansion.givenIds) {
|
|
14217
|
+
const name = graftScope.modelDef.givens?.[id]?.name;
|
|
14218
|
+
if (name)
|
|
14219
|
+
givenNamesById.set(id, name);
|
|
14220
|
+
}
|
|
14221
|
+
const givenNames = Array.from(givenNamesById.values());
|
|
13903
14222
|
const literalNames = referencedGivenNames(filterText);
|
|
13904
14223
|
const accountedFor = new Set(givenNames);
|
|
13905
14224
|
const unaccounted = literalNames.filter((name) => !accountedFor.has(name));
|
|
@@ -13910,7 +14229,11 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
|
|
|
13910
14229
|
detail: "this gate references a given id that does not resolve to a name on this model"
|
|
13911
14230
|
};
|
|
13912
14231
|
} else {
|
|
13913
|
-
classification = {
|
|
14232
|
+
classification = {
|
|
14233
|
+
shape: "row_level",
|
|
14234
|
+
givenNames,
|
|
14235
|
+
givenNamesById
|
|
14236
|
+
};
|
|
13914
14237
|
}
|
|
13915
14238
|
}
|
|
13916
14239
|
}
|
|
@@ -13936,6 +14259,14 @@ async function resolveGateShape(entry, originModelDef, graftScope, deps) {
|
|
|
13936
14259
|
if (cached.classification.shape === "rejected") {
|
|
13937
14260
|
return { shape: "rejected", cause: cached.classification.cause };
|
|
13938
14261
|
}
|
|
14262
|
+
if (entry.route === AUTHORIZE_ROUTE) {
|
|
14263
|
+
return {
|
|
14264
|
+
shape: "lock",
|
|
14265
|
+
condition: cached.condition,
|
|
14266
|
+
givenNames: cached.classification.givenNames,
|
|
14267
|
+
givenNamesById: cached.classification.givenNamesById
|
|
14268
|
+
};
|
|
14269
|
+
}
|
|
13939
14270
|
return {
|
|
13940
14271
|
shape: "row_level",
|
|
13941
14272
|
graftTarget,
|
|
@@ -14018,142 +14349,120 @@ function isBareFalseLiteral(expr) {
|
|
|
14018
14349
|
function computeGivenDeclaredTypes(givens) {
|
|
14019
14350
|
return new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
|
|
14020
14351
|
}
|
|
14021
|
-
function
|
|
14022
|
-
if (!struct || !modelDef || seen.has(struct))
|
|
14023
|
-
return [];
|
|
14024
|
-
seen.add(struct);
|
|
14025
|
-
const label = struct.as ?? struct.name;
|
|
14026
|
-
const own = collectPartitionPairs(label, ownLevelNotes(struct.annotations).map((note) => note.text));
|
|
14027
|
-
if (own.length > 0)
|
|
14028
|
-
return own;
|
|
14029
|
-
let inherited = struct.annotations?.inherits;
|
|
14030
|
-
for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
|
|
14031
|
-
const pairs = collectPartitionPairs(label, ownLevelNoteTexts(inherited));
|
|
14032
|
-
if (pairs.length > 0)
|
|
14033
|
-
return pairs;
|
|
14034
|
-
inherited = inherited.inherits;
|
|
14035
|
-
}
|
|
14036
|
-
if (inherited)
|
|
14037
|
-
throw unresolvableAncestry(label);
|
|
14038
|
-
const declared = resolveDeclaredSource(struct, modelDef);
|
|
14039
|
-
if (declared.kind === "unresolvable")
|
|
14040
|
-
throw unresolvableAncestry(label);
|
|
14041
|
-
if (declared.kind === "resolved") {
|
|
14042
|
-
const pairs = resolveEntryPointPartitions(declared.source, modelDef, seen);
|
|
14043
|
-
if (pairs.length > 0)
|
|
14044
|
-
return pairs;
|
|
14045
|
-
}
|
|
14046
|
-
const queryBase = resolveQuerySourceBase(struct, modelDef);
|
|
14047
|
-
if (queryBase)
|
|
14048
|
-
return resolveEntryPointPartitions(queryBase, modelDef, seen);
|
|
14049
|
-
return [];
|
|
14050
|
-
}
|
|
14051
|
-
function unresolvableAncestry(label) {
|
|
14052
|
-
return new PartitionAnnotationError("ancestry_unresolvable", `Could not resolve whether source "${label}" carries a ` + `\`#(partition)\` marker: the derivation chain it inherits from ` + `could not be read. Denying rather than serving unfiltered.`);
|
|
14053
|
-
}
|
|
14054
|
-
function assertPartitionAnnotationsValid(modelDef) {
|
|
14352
|
+
function assertAuthorizeGrammarValid(modelDef, authorizeMap, authorizeOwnNotes, givenDeclaredTypes, onAdmitAllGate, attributedAuthorizeOwnNotes) {
|
|
14055
14353
|
if (!modelDef)
|
|
14056
14354
|
return;
|
|
14057
|
-
for (const [
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
const
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14355
|
+
for (const [sourceName, groups] of authorizeMap) {
|
|
14356
|
+
const struct = modelDef.contents[sourceName];
|
|
14357
|
+
const ownGroupTerms = [];
|
|
14358
|
+
const declaredAdmitAllRoutes = [];
|
|
14359
|
+
for (const { route, exprs } of groups) {
|
|
14360
|
+
const isOwn = (authorizeOwnNotes.get(sourceName)?.get(route)?.length ?? 0) > 0;
|
|
14361
|
+
const filteredExprs = exprs.filter((expr) => (isOwn || expr !== "false") && !isLegacyQuotedPayload(expr));
|
|
14362
|
+
const groupTerms = [];
|
|
14363
|
+
for (const expr of filteredExprs) {
|
|
14364
|
+
const terms = parseAuthorizeGrammarBody(sourceName, expr, givenDeclaredTypes, route);
|
|
14365
|
+
for (const term of terms) {
|
|
14366
|
+
groupTerms.push({ term, route });
|
|
14367
|
+
if (term.scope === "admit_all" && (attributedAuthorizeOwnNotes?.get(sourceName)?.get(route)?.length ?? 0) > 0) {
|
|
14368
|
+
declaredAdmitAllRoutes.push(route);
|
|
14369
|
+
}
|
|
14370
|
+
if (term.scope === "row_level" && isSourceDef3(struct)) {
|
|
14371
|
+
assertNoFanoutFieldPath(sourceName, struct, term.fieldPath, term.fieldPathSegments);
|
|
14372
|
+
}
|
|
14068
14373
|
}
|
|
14069
|
-
continue;
|
|
14070
14374
|
}
|
|
14071
|
-
|
|
14072
|
-
|
|
14375
|
+
if (isOwn) {
|
|
14376
|
+
ownGroupTerms.push(...groupTerms);
|
|
14377
|
+
} else {
|
|
14378
|
+
assertAuthorizeGrammarTermsCoherent(sourceName, groupTerms);
|
|
14379
|
+
}
|
|
14073
14380
|
}
|
|
14074
|
-
if (
|
|
14075
|
-
|
|
14381
|
+
if (ownGroupTerms.length > 0) {
|
|
14382
|
+
assertAuthorizeGrammarTermsCoherent(sourceName, ownGroupTerms);
|
|
14076
14383
|
}
|
|
14077
|
-
const
|
|
14078
|
-
|
|
14079
|
-
throw new PartitionAnnotationError("partitioned_composite", `Source "${label}" is a composite source (\`compose(...)\`) with ` + `member "${marked}", which declares \`#(partition)\`. MEASURED: ` + `querying the composite reads EVERY partition of that member — ` + `the graft lands on the composite's own struct, not the ` + `resolved member branch, so no filter reaches the executed ` + `query. Remove the composite, or drop \`#(partition)\` from ` + `"${marked}" and expose it as its own source.`);
|
|
14384
|
+
for (const route of declaredAdmitAllRoutes) {
|
|
14385
|
+
onAdmitAllGate?.(sourceName, route);
|
|
14080
14386
|
}
|
|
14081
14387
|
}
|
|
14082
14388
|
}
|
|
14083
|
-
function
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
if (!reachesBelow)
|
|
14093
|
-
return;
|
|
14094
|
-
throw new PartitionAnnotationError("marker_unreachable", `Source "${label}" carries a \`#(partition)\` marker that is not ` + `declared on the source itself — it sits on a field, a view, or an ` + `inline \`compose(...)\` inside it. Nothing would filter this ` + `source: every caller would read every partition. Move the marker ` + `onto the \`source:\` declaration.`);
|
|
14095
|
-
}
|
|
14096
|
-
function partitionedMemberLabel(composite, modelDef, seen = new Set) {
|
|
14097
|
-
if (seen.has(composite))
|
|
14098
|
-
return;
|
|
14099
|
-
seen.add(composite);
|
|
14100
|
-
for (const member of composite.sources ?? []) {
|
|
14101
|
-
const label = member.as ?? member.name;
|
|
14102
|
-
if (resolveEntryPointPartitions(member, modelDef).length > 0) {
|
|
14103
|
-
return label;
|
|
14389
|
+
function fanoutSegment(struct, path) {
|
|
14390
|
+
let current = struct;
|
|
14391
|
+
for (let i = 0;i < path.length - 1; i++) {
|
|
14392
|
+
const seg = path[i];
|
|
14393
|
+
const field = (current.fields ?? []).find((f) => (f.as ?? f.name) === seg);
|
|
14394
|
+
if (!field)
|
|
14395
|
+
return { segment: seg, reason: "unresolved" };
|
|
14396
|
+
if (isJoined2(field) && field.join !== "one") {
|
|
14397
|
+
return { segment: seg, reason: "fanout" };
|
|
14104
14398
|
}
|
|
14105
|
-
if (
|
|
14106
|
-
|
|
14107
|
-
if (nested !== undefined)
|
|
14108
|
-
return nested;
|
|
14399
|
+
if (!isSourceDef3(field) && field.type !== "record" && field.type !== "array") {
|
|
14400
|
+
return { segment: seg, reason: "unresolved" };
|
|
14109
14401
|
}
|
|
14402
|
+
current = field;
|
|
14110
14403
|
}
|
|
14111
14404
|
return;
|
|
14112
14405
|
}
|
|
14113
|
-
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
const
|
|
14125
|
-
if (
|
|
14126
|
-
|
|
14127
|
-
modelPath: deps.modelPath,
|
|
14128
|
-
label
|
|
14129
|
-
});
|
|
14130
|
-
throw new Error(`partition on "${label}" resolved to no graft target`);
|
|
14406
|
+
function assertNoFanoutFieldPath(sourceName, struct, fieldPath, fieldPathSegments) {
|
|
14407
|
+
const finding = fanoutSegment(struct, fieldPathSegments);
|
|
14408
|
+
if (finding === undefined)
|
|
14409
|
+
return;
|
|
14410
|
+
const detail = finding.reason === "fanout" ? `which reaches through \`${finding.segment}\`, a fan-out join ` + `(\`join_many\`/\`join_cross\`, or a repeated record) — the ` + `entry row has many values for what follows, so there is no ` + `single key to compare against a given. Gate on a field reached ` + `only through \`join_one\` joins.` : `whose segment \`${finding.segment}\` does not resolve against ` + `this source — refused rather than assumed safe.`;
|
|
14411
|
+
throw new AuthorizeGrammarError("fanout_path", `Source "${sourceName}" declares an authorize gate on \`${fieldPath}\`, ${detail}`);
|
|
14412
|
+
}
|
|
14413
|
+
function collectRetiredRouteMarkers(modelDef) {
|
|
14414
|
+
const found = [];
|
|
14415
|
+
if (!modelDef)
|
|
14416
|
+
return found;
|
|
14417
|
+
const fileMatch = containsRetiredRouteTag(ownLevelNoteTexts(modelAnnotations(modelDef)));
|
|
14418
|
+
if (fileMatch) {
|
|
14419
|
+
found.push(`the model itself declares a retired \`##(${fileMatch})\` annotation`);
|
|
14131
14420
|
}
|
|
14132
|
-
const entries
|
|
14133
|
-
|
|
14134
|
-
if (!
|
|
14135
|
-
|
|
14136
|
-
|
|
14421
|
+
for (const [key, obj] of Object.entries(modelDef.contents)) {
|
|
14422
|
+
const isQuery = obj.type === "query";
|
|
14423
|
+
if (!isSourceDef3(obj) && !isQuery)
|
|
14424
|
+
continue;
|
|
14425
|
+
const label = obj.as ?? obj.name ?? key;
|
|
14426
|
+
const noun = isQuery ? "query" : "source";
|
|
14427
|
+
const ownMatch = containsRetiredRouteTag(annotationTexts(obj.annotations) ?? []);
|
|
14428
|
+
if (ownMatch) {
|
|
14429
|
+
found.push(`${noun} "${label}" declares a retired \`#(${ownMatch})\` annotation`);
|
|
14430
|
+
continue;
|
|
14431
|
+
}
|
|
14432
|
+
if (isQuery) {
|
|
14433
|
+
continue;
|
|
14434
|
+
}
|
|
14435
|
+
let belowMatch;
|
|
14436
|
+
try {
|
|
14437
|
+
belowMatch = reachesRetiredRouteTagBelow(obj);
|
|
14438
|
+
} catch {
|
|
14439
|
+
belowMatch = RETIRED_ROUTES[0];
|
|
14440
|
+
}
|
|
14441
|
+
if (belowMatch) {
|
|
14442
|
+
found.push(`source "${label}" carries a retired \`#(${belowMatch})\` ` + `annotation somewhere in its IR (not on the source line itself)`);
|
|
14137
14443
|
}
|
|
14138
|
-
const filterText = `${pair.column} = $${pair.given}`;
|
|
14139
|
-
const condition = await liftGateCondition(graftTarget, filterText, graftScope.materializer);
|
|
14140
|
-
entries.push({
|
|
14141
|
-
label,
|
|
14142
|
-
graftTarget,
|
|
14143
|
-
filterText,
|
|
14144
|
-
condition,
|
|
14145
|
-
givenNames: [pair.given]
|
|
14146
|
-
});
|
|
14147
14444
|
}
|
|
14148
|
-
return
|
|
14445
|
+
return found;
|
|
14446
|
+
}
|
|
14447
|
+
function assertNoRetiredRouteMarkers(found) {
|
|
14448
|
+
if (found.length === 0)
|
|
14449
|
+
return;
|
|
14450
|
+
throw new ModelCompilationError({
|
|
14451
|
+
message: `These sources carry a marker from a retired annotation route:
|
|
14452
|
+
` + `${found.map((f) => ` - ${f}`).join(`
|
|
14453
|
+
`)}
|
|
14454
|
+
` + `The route is no longer enforced by anything, so Malloy would ` + `otherwise parse it, load the model clean, and serve every row as ` + `if the marker were never written. Remove the annotation.`
|
|
14455
|
+
});
|
|
14149
14456
|
}
|
|
14150
14457
|
var init_gate_classification = __esm(() => {
|
|
14458
|
+
init_errors();
|
|
14151
14459
|
init_logger();
|
|
14152
14460
|
init_annotations();
|
|
14153
14461
|
init_authorize();
|
|
14462
|
+
init_authorize_grammar();
|
|
14463
|
+
init_authorize_routes();
|
|
14154
14464
|
init_gate_dimension();
|
|
14155
14465
|
init_gate_registry_walk();
|
|
14156
|
-
init_partition_annotation();
|
|
14157
14466
|
});
|
|
14158
14467
|
|
|
14159
14468
|
// src/service/filter.ts
|
|
@@ -14350,7 +14659,7 @@ var init_filter = __esm(() => {
|
|
|
14350
14659
|
|
|
14351
14660
|
// src/service/source_extraction.ts
|
|
14352
14661
|
import {
|
|
14353
|
-
isJoined as
|
|
14662
|
+
isJoined as isJoined3,
|
|
14354
14663
|
isSourceDef as isSourceDef4,
|
|
14355
14664
|
modelDefToModelInfo
|
|
14356
14665
|
} from "@malloydata/malloy";
|
|
@@ -14404,7 +14713,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14404
14713
|
considerNoteOwner(authorizeNoteDeclaredBy, note, struct);
|
|
14405
14714
|
}
|
|
14406
14715
|
}
|
|
14407
|
-
nearMissAuthorize.push(...
|
|
14716
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
|
|
14408
14717
|
...ownLevelNotes(struct.annotations),
|
|
14409
14718
|
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
14410
14719
|
].map((note) => note.text)));
|
|
@@ -14422,23 +14731,29 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14422
14731
|
considerNoteOwner(authorizeNoteDeclaredBy, note, entry);
|
|
14423
14732
|
}
|
|
14424
14733
|
}
|
|
14425
|
-
nearMissAuthorize.push(...
|
|
14734
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes(ownLevelNotes(entry.annotations).map((note) => note.text)));
|
|
14426
14735
|
}
|
|
14427
14736
|
for (const struct of derivedStructsReachable(sweptStructs, modelDef)) {
|
|
14428
|
-
nearMissAuthorize.push(...
|
|
14737
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([
|
|
14429
14738
|
...ownLevelNotes(struct.annotations),
|
|
14430
14739
|
...struct.fields.flatMap((field) => ownLevelNotes(field.annotations))
|
|
14431
14740
|
].map((note) => note.text)));
|
|
14432
14741
|
}
|
|
14433
14742
|
{
|
|
14434
14743
|
const folded = modelAnnotations(modelDef);
|
|
14435
|
-
nearMissAuthorize.push(...
|
|
14744
|
+
nearMissAuthorize.push(...collectAuthorizeNearMissesAllRoutes([...folded.notes ?? [], ...folded.blockNotes ?? []].map((note) => note.text)));
|
|
14436
14745
|
}
|
|
14437
14746
|
assertNoAuthorizeNearMisses(nearMissAuthorize);
|
|
14438
14747
|
const authorizeOwnNotes = new Map;
|
|
14439
14748
|
const attributedAuthorizeOwnNotes = new Map;
|
|
14440
|
-
|
|
14441
|
-
|
|
14749
|
+
{
|
|
14750
|
+
const fileNoteTexts = ownLevelNoteTexts(modelAnnotations(modelDef));
|
|
14751
|
+
if (containsAuthorizeAnnotationTag(fileNoteTexts)) {
|
|
14752
|
+
misplacedAuthorize.push({
|
|
14753
|
+
kind: "file",
|
|
14754
|
+
route: fileNoteTexts.map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14755
|
+
});
|
|
14756
|
+
}
|
|
14442
14757
|
}
|
|
14443
14758
|
const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef4(obj)).map((sourceObj) => {
|
|
14444
14759
|
const struct = sourceObj;
|
|
@@ -14477,17 +14792,32 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14477
14792
|
}
|
|
14478
14793
|
}
|
|
14479
14794
|
const ownNotes = ownLevelNoteTexts(struct.annotations);
|
|
14480
|
-
const ownGates = collectAuthorizeExprs(ownNotes);
|
|
14481
14795
|
const ownAuthorizeNotes = ownLevelNotes(struct.annotations).filter((note) => containsAuthorizeAnnotationTag([note.text]));
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
const
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14796
|
+
const ownNotesByRoute = new Map;
|
|
14797
|
+
const attributedOwnNotesByRoute = new Map;
|
|
14798
|
+
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
14799
|
+
const notesForRoute = ownAuthorizeNotes.filter((note) => authorizeAnnotationRoute(note.text) === route);
|
|
14800
|
+
ownNotesByRoute.set(route, notesForRoute);
|
|
14801
|
+
attributedOwnNotesByRoute.set(route, notesForRoute.filter((note) => authorizeNoteDeclaredBy.get(note) === struct));
|
|
14802
|
+
}
|
|
14803
|
+
authorizeOwnNotes.set(sourceName, ownNotesByRoute);
|
|
14804
|
+
attributedAuthorizeOwnNotes.set(sourceName, attributedOwnNotesByRoute);
|
|
14805
|
+
const groupsForSource = [];
|
|
14806
|
+
const routeGroupsByRoute = new Map;
|
|
14807
|
+
for (const route of CANONICAL_AUTHORIZE_ROUTES) {
|
|
14808
|
+
const ownExprsForRoute = collectAuthorizeExprsForRoute(ownNotes, route);
|
|
14809
|
+
const groupsForRoute = ownExprsForRoute.length > 0 ? [ownExprsForRoute] : effectiveAncestorGateExprs(struct, modelDef, route);
|
|
14810
|
+
for (const exprs of groupsForRoute) {
|
|
14811
|
+
groupsForSource.push({ route, exprs });
|
|
14812
|
+
}
|
|
14813
|
+
routeGroupsByRoute.set(route, groupsForRoute);
|
|
14814
|
+
}
|
|
14815
|
+
if (groupsForSource.length > 0) {
|
|
14816
|
+
authorizeMap.set(sourceName, groupsForSource);
|
|
14817
|
+
}
|
|
14818
|
+
const routeGroupsToFlatWire = (groups) => groups && groups.length > 0 ? groups.flat() : undefined;
|
|
14819
|
+
const authorize = routeGroupsToFlatWire(routeGroupsByRoute.get(AUTHORIZE_ROUTE));
|
|
14820
|
+
const accessFilter = routeGroupsToFlatWire(routeGroupsByRoute.get(ACCESS_FILTER_ROUTE));
|
|
14491
14821
|
const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
|
|
14492
14822
|
name: turtle.as || turtle.name,
|
|
14493
14823
|
annotations: annotationTexts(turtle.annotations)
|
|
@@ -14500,7 +14830,7 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14500
14830
|
continue;
|
|
14501
14831
|
}
|
|
14502
14832
|
const fieldName = field.as || field.name;
|
|
14503
|
-
if (
|
|
14833
|
+
if (isJoined3(field) && isSourceDef4(field)) {
|
|
14504
14834
|
const joinedStruct = field;
|
|
14505
14835
|
if (joinFieldNamesUnresolvableDeclaration(joinedStruct, modelDef)) {
|
|
14506
14836
|
continue;
|
|
@@ -14508,14 +14838,16 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14508
14838
|
misplacedAuthorize.push({
|
|
14509
14839
|
kind: "field",
|
|
14510
14840
|
name: sourceName,
|
|
14511
|
-
fieldName
|
|
14841
|
+
fieldName,
|
|
14842
|
+
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14512
14843
|
});
|
|
14513
14844
|
continue;
|
|
14514
14845
|
}
|
|
14515
14846
|
misplacedAuthorize.push({
|
|
14516
14847
|
kind: "field",
|
|
14517
14848
|
name: sourceName,
|
|
14518
|
-
fieldName
|
|
14849
|
+
fieldName,
|
|
14850
|
+
route: fieldAuthorizeNotes.map((note) => authorizeAnnotationRoute(note.text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14519
14851
|
});
|
|
14520
14852
|
}
|
|
14521
14853
|
return {
|
|
@@ -14524,7 +14856,8 @@ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
|
14524
14856
|
views,
|
|
14525
14857
|
filters,
|
|
14526
14858
|
givens,
|
|
14527
|
-
authorize
|
|
14859
|
+
authorize,
|
|
14860
|
+
accessFilter
|
|
14528
14861
|
};
|
|
14529
14862
|
});
|
|
14530
14863
|
return {
|
|
@@ -14541,7 +14874,8 @@ function extractQueriesFromModelDef(modelDef) {
|
|
|
14541
14874
|
const namedQueries = Object.values(modelDef.contents).filter(isNamedQuery);
|
|
14542
14875
|
const misplacedAuthorize = namedQueries.filter((queryObj) => containsAuthorizeAnnotationTag(ownLevelNoteTexts(queryObj.annotations))).map((queryObj) => ({
|
|
14543
14876
|
kind: "query",
|
|
14544
|
-
name: queryObj.as || queryObj.name
|
|
14877
|
+
name: queryObj.as || queryObj.name,
|
|
14878
|
+
route: ownLevelNoteTexts(queryObj.annotations).map((text) => authorizeAnnotationRoute(text)).find((r) => r !== undefined) ?? ACCESS_FILTER_ROUTE
|
|
14545
14879
|
}));
|
|
14546
14880
|
const queries = namedQueries.map((queryObj) => ({
|
|
14547
14881
|
name: queryObj.as || queryObj.name,
|
|
@@ -14553,6 +14887,7 @@ function extractQueriesFromModelDef(modelDef) {
|
|
|
14553
14887
|
var init_source_extraction = __esm(() => {
|
|
14554
14888
|
init_annotations();
|
|
14555
14889
|
init_authorize();
|
|
14890
|
+
init_authorize_routes();
|
|
14556
14891
|
init_filter();
|
|
14557
14892
|
init_gate_registry_walk();
|
|
14558
14893
|
});
|
|
@@ -14918,7 +15253,13 @@ function collectGivenRefs(value, into) {
|
|
|
14918
15253
|
for (const child of Object.values(node))
|
|
14919
15254
|
collectGivenRefs(child, into);
|
|
14920
15255
|
}
|
|
14921
|
-
function
|
|
15256
|
+
function gateGivenSource(sources, name) {
|
|
15257
|
+
const source = sources.find((candidate) => candidate.name === name);
|
|
15258
|
+
if (!source)
|
|
15259
|
+
return;
|
|
15260
|
+
return [...source.authorize ?? [], ...source.accessFilter ?? []];
|
|
15261
|
+
}
|
|
15262
|
+
function suggestGivenLookup(modelDef, gatesBySource, surfaced) {
|
|
14922
15263
|
const registry = modelDef.givens ?? {};
|
|
14923
15264
|
const bySource = new Map;
|
|
14924
15265
|
const byQuery = new Map;
|
|
@@ -14927,7 +15268,7 @@ function suggestGivenLookup(modelDef, authorizeBySource, surfaced) {
|
|
|
14927
15268
|
const name = obj.as || obj.name;
|
|
14928
15269
|
const refs = new Set;
|
|
14929
15270
|
collectGivenRefs(obj.filterList, refs);
|
|
14930
|
-
for (const expr of
|
|
15271
|
+
for (const expr of gatesBySource(name) ?? []) {
|
|
14931
15272
|
for (const given of referencedGivenNames(expr))
|
|
14932
15273
|
refs.add(given);
|
|
14933
15274
|
}
|
|
@@ -15028,6 +15369,7 @@ import { parentPort, threadId } from "node:worker_threads";
|
|
|
15028
15369
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
15029
15370
|
|
|
15030
15371
|
// src/service/package_manifest.ts
|
|
15372
|
+
init_constants();
|
|
15031
15373
|
var FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"];
|
|
15032
15374
|
var PACKAGE_SCOPES = ["version", "package"];
|
|
15033
15375
|
function parsePackageScope(raw) {
|
|
@@ -15155,6 +15497,48 @@ function materializationWithQueryMetadata(parsed, queryMetadataRaw) {
|
|
|
15155
15497
|
function queryMetadataParseWarnings(raw, home = "root") {
|
|
15156
15498
|
return parseQueryMetadata(raw, QUERY_METADATA_HOME_LABELS[home]).warnings;
|
|
15157
15499
|
}
|
|
15500
|
+
function resolveExplores(input) {
|
|
15501
|
+
const { declaredExplores, declaredQueryableSources, modelPaths } = input;
|
|
15502
|
+
const hasIndexModel = modelPaths.includes(INDEX_MODEL_NAME);
|
|
15503
|
+
const warnings = [];
|
|
15504
|
+
if (declaredExplores !== undefined && !(Array.isArray(declaredExplores) && declaredExplores.every((entry) => typeof entry === "string"))) {
|
|
15505
|
+
throw new Error(`Invalid "explores" in publisher.json: expected an array of model ` + `paths, got ${JSON.stringify(declaredExplores)}. The package is ` + `not served, because ignoring the key would publish every source ` + `it was meant to withhold. Fix: "explores": ` + `["${INDEX_MODEL_NAME}"], or delete the key to use a root ` + `"${INDEX_MODEL_NAME}" instead.`);
|
|
15506
|
+
}
|
|
15507
|
+
const declared = Array.isArray(declaredExplores) ? declaredExplores.map((entry) => normalizeModelPath(entry)) : undefined;
|
|
15508
|
+
if (declaredQueryableSources !== undefined && declaredQueryableSources !== "all") {
|
|
15509
|
+
warnings.push(QUERYABLE_SOURCES_DEPRECATION);
|
|
15510
|
+
}
|
|
15511
|
+
if (declared !== undefined) {
|
|
15512
|
+
if (declared.length === 0 && !hasIndexModel) {
|
|
15513
|
+
warnings.push(EXPLORES_EMPTY_IS_UNCURATED);
|
|
15514
|
+
} else if (declared.length === 0) {
|
|
15515
|
+
warnings.push(EXPLORES_EMPTY_SUPPRESSES_CONVENTION);
|
|
15516
|
+
} else if (declared.length === 1) {
|
|
15517
|
+
warnings.push(EXPLORES_DEPRECATION);
|
|
15518
|
+
}
|
|
15519
|
+
if (hasIndexModel && declared.length > 0) {
|
|
15520
|
+
if (!declared.includes(INDEX_MODEL_NAME)) {
|
|
15521
|
+
warnings.push(exploresOmitsIndexModel(declared));
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
return { explores: declared, warnings };
|
|
15525
|
+
}
|
|
15526
|
+
if (!hasIndexModel) {
|
|
15527
|
+
return { explores: undefined, warnings };
|
|
15528
|
+
}
|
|
15529
|
+
if (modelPaths.some((modelPath) => modelPath.endsWith(MODEL_FILE_SUFFIX) && modelPath !== INDEX_MODEL_NAME)) {
|
|
15530
|
+
warnings.push(INDEX_MODEL_IS_THE_SURFACE);
|
|
15531
|
+
}
|
|
15532
|
+
return { explores: [INDEX_MODEL_NAME], warnings };
|
|
15533
|
+
}
|
|
15534
|
+
var INDEX_MODEL_IS_THE_SURFACE = `This package has a root "${INDEX_MODEL_NAME}" and no "explores" in ` + `publisher.json, so that file is its published surface: only it is listed, ` + `and sources it does not "export { ... }" are refused by name with a 404. ` + `Other models still compile and are still importable and joinable. If that ` + `is intended, nothing to do. If this package is not meant to be curated, ` + `add "explores": [] to publisher.json, which keeps every model listed and ` + `queryable. Do NOT rename or delete the file to opt out: that widens the ` + `surface silently and breaks every import that names it.`;
|
|
15535
|
+
function exploresOmitsIndexModel(declared) {
|
|
15536
|
+
return `This package has an "${INDEX_MODEL_NAME}" but its publisher.json ` + `"explores" does not list it (${JSON.stringify(declared)}). The ` + `explicit key wins, so "${INDEX_MODEL_NAME}" is neither listed nor ` + `queryable. If that is intended, rename the file to silence this. If you ` + `meant it to be the package's entry point, delete "explores" and let the ` + `convention use it, or add it to the list.`;
|
|
15537
|
+
}
|
|
15538
|
+
var EXPLORES_DEPRECATION = `"explores" in publisher.json is deprecated: put an "${INDEX_MODEL_NAME}" ` + `at the package root that imports your models and "export { ... }"s what ` + `you publish, then delete the key. The file is the surface, it is checked ` + `by the compiler rather than by a path list, and it curates and enforces ` + `exactly as the key does. The key still works and is not going away in this ` + `release.`;
|
|
15539
|
+
var EXPLORES_EMPTY_SUPPRESSES_CONVENTION = `"explores": [] in publisher.json is keeping this package uncurated. It has ` + `an "${INDEX_MODEL_NAME}", which would otherwise be its published surface: ` + `only that file would be listed, and sources it does not export would stop ` + `answering by name. The empty array suppresses that, so every model stays ` + `listed and queryable. This is supported and is the intended way to opt out ` + `-- do NOT delete the key unless you want the convention to take effect.`;
|
|
15540
|
+
var EXPLORES_EMPTY_IS_UNCURATED = `"explores": [] in publisher.json is keeping this package uncurated: every ` + `model is listed and queryable by name. This is supported and is the ` + `intended way to say "do not curate". Nothing to do unless you meant to ` + `publish a surface, in which case list the models, or add a root ` + `"${INDEX_MODEL_NAME}" and delete the key.`;
|
|
15541
|
+
var QUERYABLE_SOURCES_DEPRECATION = `"queryableSources" in publisher.json is deprecated. "declared" is already ` + `the default, so the key changes nothing, and an "${INDEX_MODEL_NAME}" ` + `gives the same curated-and-enforced surface with no manifest field at all. ` + `Delete it. The key still works and is not going away in this release.`;
|
|
15158
15542
|
|
|
15159
15543
|
// src/package_load/package_load_worker.ts
|
|
15160
15544
|
init_source_extraction();
|
|
@@ -15389,21 +15773,27 @@ function buildWorkerMalloyConfig(job) {
|
|
|
15389
15773
|
}));
|
|
15390
15774
|
return config;
|
|
15391
15775
|
}
|
|
15392
|
-
async function readPackageMetadata(packagePath) {
|
|
15776
|
+
async function readPackageMetadata(packagePath, modelPaths) {
|
|
15393
15777
|
const manifestPath = path2.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
15394
15778
|
const contents = await fs2.promises.readFile(manifestPath, "utf8");
|
|
15395
15779
|
const parsed = JSON.parse(contents);
|
|
15396
15780
|
const scope = resolvePackageScope(parsed.scope, parsed.materialization);
|
|
15397
15781
|
const queryMetadata = resolvePackageQueryMetadata(parsed.queryMetadata, parsed.materialization);
|
|
15782
|
+
const explores = resolveExplores({
|
|
15783
|
+
declaredExplores: parsed.explores,
|
|
15784
|
+
declaredQueryableSources: parsed.queryableSources,
|
|
15785
|
+
modelPaths
|
|
15786
|
+
});
|
|
15398
15787
|
const manifestWarnings = [
|
|
15399
15788
|
...scope.warnings,
|
|
15400
15789
|
...queryMetadata.warnings,
|
|
15790
|
+
...explores.warnings,
|
|
15401
15791
|
...queryMetadataParseWarnings(queryMetadata.queryMetadata, queryMetadata.home)
|
|
15402
15792
|
];
|
|
15403
15793
|
return {
|
|
15404
15794
|
name: parsed.name,
|
|
15405
15795
|
description: parsed.description,
|
|
15406
|
-
explores:
|
|
15796
|
+
explores: explores.explores,
|
|
15407
15797
|
queryableSources: parsed.queryableSources === "all" ? "all" : "declared",
|
|
15408
15798
|
manifestLocation: typeof parsed.manifestLocation === "string" ? parsed.manifestLocation : null,
|
|
15409
15799
|
materialization: materializationWithQueryMetadata(parsePackageMaterialization(parsed.materialization), queryMetadata.queryMetadata),
|
|
@@ -15441,7 +15831,7 @@ function authorizeWarningCollector() {
|
|
|
15441
15831
|
return {
|
|
15442
15832
|
warnings,
|
|
15443
15833
|
onRowLevelGateUnexpressible: (sourceName, detail) => {
|
|
15444
|
-
warnings.push(`Row-level #(
|
|
15834
|
+
warnings.push(`Row-level #(access_filter) gate not expressible at entry point "${sourceName}"; every query against it will be denied: ${detail}`);
|
|
15445
15835
|
}
|
|
15446
15836
|
};
|
|
15447
15837
|
}
|
|
@@ -15500,10 +15890,10 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
|
|
|
15500
15890
|
authorizeOwnNotes,
|
|
15501
15891
|
attributedAuthorizeOwnNotes
|
|
15502
15892
|
} = extractSources(modelDef, givens);
|
|
15503
|
-
attachSuggestGivenNames(givens, suggestGivenLookup(modelDef, (name) => sources
|
|
15893
|
+
attachSuggestGivenNames(givens, suggestGivenLookup(modelDef, (name) => gateGivenSource(sources, name), new Set((givens ?? []).map((given) => given.name))));
|
|
15504
15894
|
const queryResult = extractQueries(modelDef);
|
|
15505
15895
|
const queries = queryResult.queries;
|
|
15506
|
-
|
|
15896
|
+
assertNoRetiredRouteMarkers(collectRetiredRouteMarkers(modelDef));
|
|
15507
15897
|
assertNoMisplacedAuthorizeAnnotations([
|
|
15508
15898
|
...misplacedAuthorize,
|
|
15509
15899
|
...queryResult.misplacedAuthorize
|
|
@@ -15511,20 +15901,20 @@ async function compileMalloyModel(job, malloyConfig, modelPath) {
|
|
|
15511
15901
|
const legacyStringGates = findLegacyStringGates(authorizeOwnNotes);
|
|
15512
15902
|
legacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
|
|
15513
15903
|
assertNoLegacyStringGate(legacyStringGates);
|
|
15514
|
-
|
|
15904
|
+
assertAuthorizeGrammarValid(modelDef, authorizeMap, authorizeOwnNotes, computeGivenDeclaredTypes(givens), (_sourceName, route) => recordAuthorizeAdmitAllGate(route), attributedAuthorizeOwnNotes);
|
|
15515
15905
|
const authorizeWarningCollection = authorizeWarningCollector();
|
|
15516
15906
|
await validateAuthorizeProbes(mm, {
|
|
15517
15907
|
authorizeMap,
|
|
15518
15908
|
authorizeOwnNotes: attributedAuthorizeOwnNotes,
|
|
15519
15909
|
onRowLevelGateRejected: recordRowLevelGateRejected,
|
|
15520
15910
|
onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
|
|
15521
|
-
onOwnRowLevelConditionCompiled: (sourceName, condition) => {
|
|
15911
|
+
onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
|
|
15522
15912
|
const struct = modelDef.contents[sourceName];
|
|
15523
15913
|
if (!struct || !isSourceDef6(struct))
|
|
15524
15914
|
return;
|
|
15525
|
-
validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
|
|
15915
|
+
validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, modelDef, (cause, detail) => {
|
|
15526
15916
|
recordRowLevelGateRejected(cause);
|
|
15527
|
-
authorizeWarningCollection.warnings.push(
|
|
15917
|
+
authorizeWarningCollection.warnings.push(`#(${route}) gate warning on "${sourceName}" (${cause}): ${detail}`);
|
|
15528
15918
|
});
|
|
15529
15919
|
}
|
|
15530
15920
|
});
|
|
@@ -15624,11 +16014,11 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
|
|
|
15624
16014
|
finalSourceInfos = collectSourceInfos(finalModelDef);
|
|
15625
16015
|
const extracted = extractSources(finalModelDef, finalGivens);
|
|
15626
16016
|
finalSources = extracted.sources;
|
|
15627
|
-
attachSuggestGivenNames(finalGivens, suggestGivenLookup(finalModelDef, (name) => extracted.sources
|
|
16017
|
+
attachSuggestGivenNames(finalGivens, suggestGivenLookup(finalModelDef, (name) => gateGivenSource(extracted.sources, name), new Set((finalGivens ?? []).map((given) => given.name))));
|
|
15628
16018
|
finalFilterMap = extracted.filterMap;
|
|
15629
16019
|
const finalQueryResult = extractQueries(finalModelDef);
|
|
15630
16020
|
finalQueries = finalQueryResult.queries;
|
|
15631
|
-
|
|
16021
|
+
assertNoRetiredRouteMarkers(collectRetiredRouteMarkers(finalModelDef));
|
|
15632
16022
|
assertNoMisplacedAuthorizeAnnotations([
|
|
15633
16023
|
...extracted.misplacedAuthorize,
|
|
15634
16024
|
...finalQueryResult.misplacedAuthorize
|
|
@@ -15636,20 +16026,20 @@ async function compileNotebookModel(job, malloyConfig, modelPath) {
|
|
|
15636
16026
|
const finalLegacyStringGates = findLegacyStringGates(extracted.authorizeOwnNotes);
|
|
15637
16027
|
finalLegacyStringGates.forEach(() => recordRowLevelGateRejected("legacy_string_gate"));
|
|
15638
16028
|
assertNoLegacyStringGate(finalLegacyStringGates);
|
|
15639
|
-
|
|
16029
|
+
assertAuthorizeGrammarValid(finalModelDef, extracted.authorizeMap, extracted.authorizeOwnNotes, computeGivenDeclaredTypes(finalGivens), (_sourceName, route) => recordAuthorizeAdmitAllGate(route), extracted.attributedAuthorizeOwnNotes);
|
|
15640
16030
|
const finalCompiledModelDef = finalModelDef;
|
|
15641
16031
|
await validateAuthorizeProbes(mm, {
|
|
15642
16032
|
authorizeMap: extracted.authorizeMap,
|
|
15643
16033
|
authorizeOwnNotes: extracted.attributedAuthorizeOwnNotes,
|
|
15644
16034
|
onRowLevelGateRejected: recordRowLevelGateRejected,
|
|
15645
16035
|
onRowLevelGateUnexpressible: authorizeWarningCollection.onRowLevelGateUnexpressible,
|
|
15646
|
-
onOwnRowLevelConditionCompiled: (sourceName, condition) => {
|
|
16036
|
+
onOwnRowLevelConditionCompiled: (sourceName, condition, route) => {
|
|
15647
16037
|
const struct = finalCompiledModelDef.contents[sourceName];
|
|
15648
16038
|
if (!struct || !isSourceDef6(struct))
|
|
15649
16039
|
return;
|
|
15650
|
-
validateSourceLineGateGivenUsage(sourceName, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
|
|
16040
|
+
validateSourceLineGateGivenUsage(sourceName, route, struct, condition.refSummary, condition.e, finalCompiledModelDef, (cause, detail) => {
|
|
15651
16041
|
recordRowLevelGateRejected(cause);
|
|
15652
|
-
authorizeWarningCollection.warnings.push(
|
|
16042
|
+
authorizeWarningCollection.warnings.push(`#(${route}) gate warning on "${sourceName}" (${cause}): ${detail}`);
|
|
15653
16043
|
});
|
|
15654
16044
|
}
|
|
15655
16045
|
});
|
|
@@ -15698,10 +16088,10 @@ async function compileOneModel(job, malloyConfig, modelPath) {
|
|
|
15698
16088
|
}
|
|
15699
16089
|
async function loadPackage(job) {
|
|
15700
16090
|
const loadStart = performance.now();
|
|
15701
|
-
const packageMetadata = await readPackageMetadata(job.packagePath);
|
|
15702
|
-
const malloyConfig = buildWorkerMalloyConfig(job);
|
|
15703
16091
|
const allFiles = await listPackageFiles(job.packagePath);
|
|
15704
16092
|
const modelPaths = filterModelPaths(allFiles);
|
|
16093
|
+
const packageMetadata = await readPackageMetadata(job.packagePath, modelPaths);
|
|
16094
|
+
const malloyConfig = buildWorkerMalloyConfig(job);
|
|
15705
16095
|
const replacementMatchedExisting = job.replacement ? modelPaths.includes(job.replacement.modelPath) : undefined;
|
|
15706
16096
|
if (job.replacement && !replacementMatchedExisting) {
|
|
15707
16097
|
modelPaths.push(job.replacement.modelPath);
|