@malloy-publisher/server 0.0.241 → 0.0.243

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.
@@ -4141,25 +4141,31 @@ components:
4141
4141
  type: string
4142
4142
  description: Snowflake username for authentication
4143
4143
  password:
4144
- type: string
4144
+ type: ["string", "null"]
4145
4145
  description: Snowflake password for authentication
4146
4146
  privateKey:
4147
4147
  type: string
4148
4148
  description: Snowflake private key for authentication
4149
4149
  privateKeyPass:
4150
- type: string
4150
+ type: ["string", "null"]
4151
4151
  description: Passphrase for the Snowflake private key
4152
4152
  warehouse:
4153
4153
  type: string
4154
4154
  description: Snowflake warehouse name
4155
4155
  database:
4156
- type: string
4157
- description: Snowflake database name
4156
+ type: ["string", "null"]
4157
+ description:
4158
+ Snowflake database name. Omitted or null connects with no current
4159
+ database, so names must be fully qualified as
4160
+ DATABASE.SCHEMA.TABLE and schema discovery spans the account.
4158
4161
  schema:
4159
- type: string
4160
- description: Snowflake schema name
4162
+ type: ["string", "null"]
4163
+ description:
4164
+ Snowflake schema name. Sets the session default schema and forms part
4165
+ of the connection's identity, so it determines what a materialization
4166
+ build runs as. It does not restrict which schemas are discoverable.
4161
4167
  role:
4162
- type: string
4168
+ type: ["string", "null"]
4163
4169
  description: Snowflake role name
4164
4170
  responseTimeoutMilliseconds:
4165
4171
  type: integer
package/dist/server.mjs CHANGED
@@ -265609,6 +265609,9 @@ var PROXIED_SSLMODES = [
265609
265609
  "verify-full"
265610
265610
  ];
265611
265611
  var PUBLISHER_DUCKDB_API_FIELDS = new Set(["attachedDatabases"]);
265612
+ function nullToUndefined(value) {
265613
+ return value ?? undefined;
265614
+ }
265612
265615
  function normalizeSnowflakePrivateKey(privateKey) {
265613
265616
  let privateKeyContent = privateKey.trim();
265614
265617
  if (!privateKeyContent.includes(`
@@ -266129,13 +266132,13 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
266129
266132
  is: "snowflake",
266130
266133
  account: connection.snowflakeConnection?.account,
266131
266134
  username: connection.snowflakeConnection?.username,
266132
- password: connection.snowflakeConnection?.password,
266135
+ password: nullToUndefined(connection.snowflakeConnection?.password),
266133
266136
  privateKey: connection.snowflakeConnection?.privateKey ? normalizeSnowflakePrivateKey(connection.snowflakeConnection.privateKey) : undefined,
266134
- privateKeyPass: connection.snowflakeConnection?.privateKeyPass,
266137
+ privateKeyPass: nullToUndefined(connection.snowflakeConnection?.privateKeyPass),
266135
266138
  warehouse: connection.snowflakeConnection?.warehouse,
266136
- database: connection.snowflakeConnection?.database,
266137
- schema: connection.snowflakeConnection?.schema,
266138
- role: connection.snowflakeConnection?.role,
266139
+ database: nullToUndefined(connection.snowflakeConnection?.database),
266140
+ schema: nullToUndefined(connection.snowflakeConnection?.schema),
266141
+ role: nullToUndefined(connection.snowflakeConnection?.role),
266139
266142
  timeoutMs: connection.snowflakeConnection?.responseTimeoutMilliseconds,
266140
266143
  poolMin: 1,
266141
266144
  poolMax: 20
@@ -266657,32 +266660,44 @@ async function federateSnowflake(connection, config) {
266657
266660
  if (!sf) {
266658
266661
  throw new Error(`Snowflake connection configuration missing for: ${config.name}`);
266659
266662
  }
266660
- const required = {
266663
+ for (const [field, value] of Object.entries({
266661
266664
  account: sf.account,
266662
- username: sf.username,
266663
- password: sf.password
266664
- };
266665
- for (const [field, value] of Object.entries(required)) {
266665
+ username: sf.username
266666
+ })) {
266666
266667
  if (!value) {
266667
266668
  throw new Error(`Snowflake ${field} is required for: ${config.name}`);
266668
266669
  }
266669
266670
  }
266671
+ const usesKeyPair = !!sf.privateKey;
266672
+ if (!usesKeyPair && !sf.password) {
266673
+ throw new Error(`Snowflake privateKey or password is required for: ${config.name}`);
266674
+ }
266670
266675
  await installAndLoadExtension(connection, "snowflake", true);
266671
266676
  const params = {
266672
266677
  account: escapeSQL(sf.account || ""),
266673
266678
  user: escapeSQL(sf.username || ""),
266674
- password: escapeSQL(sf.password || ""),
266679
+ password: sf.password ? escapeSQL(sf.password) : undefined,
266680
+ privateKey: sf.privateKey ? escapeSQL(normalizeSnowflakePrivateKey(sf.privateKey)) : undefined,
266681
+ privateKeyPass: sf.privateKeyPass ? escapeSQL(sf.privateKeyPass) : undefined,
266675
266682
  database: sf.database ? escapeSQL(sf.database) : undefined,
266676
- warehouse: sf.warehouse ? escapeSQL(sf.warehouse) : undefined
266683
+ warehouse: sf.warehouse ? escapeSQL(sf.warehouse) : undefined,
266684
+ schema: sf.schema ? escapeSQL(sf.schema) : undefined,
266685
+ role: sf.role ? escapeSQL(sf.role) : undefined
266677
266686
  };
266678
266687
  const secretName = sanitizeSecretName(`snowflake_${config.name}`);
266679
266688
  const secretLines = [
266680
266689
  ` TYPE snowflake`,
266681
266690
  ` ACCOUNT '${params.account}'`,
266682
266691
  ` USER '${params.user}'`,
266683
- ` PASSWORD '${params.password}'`,
266692
+ ...usesKeyPair ? [
266693
+ ` AUTH_TYPE 'key_pair'`,
266694
+ ` PRIVATE_KEY '${params.privateKey}'`,
266695
+ ...params.privateKeyPass ? [` PRIVATE_KEY_PASSWORD '${params.privateKeyPass}'`] : []
266696
+ ] : [` PASSWORD '${params.password}'`],
266684
266697
  ...params.database ? [` DATABASE '${params.database}'`] : [],
266685
- ...params.warehouse ? [` WAREHOUSE '${params.warehouse}'`] : []
266698
+ ...params.warehouse ? [` WAREHOUSE '${params.warehouse}'`] : [],
266699
+ ...params.schema ? [` SCHEMA '${params.schema}'`] : [],
266700
+ ...params.role ? [` ROLE '${params.role}'`] : []
266686
266701
  ];
266687
266702
  await connection.runSQL(`CREATE OR REPLACE SECRET ${secretName} (
266688
266703
  ${secretLines.join(`,
@@ -267004,7 +267019,7 @@ function entryToDuckDBOptions(name, entry, workingDirectory) {
267004
267019
  return { ...removeUndefined(rest), name };
267005
267020
  }
267006
267021
  function removeUndefined(value) {
267007
- return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== undefined));
267022
+ return Object.fromEntries(Object.entries(value).filter(([, fieldValue]) => fieldValue !== undefined && fieldValue !== null));
267008
267023
  }
267009
267024
  function buildSnowflakePrivateKeyConnection(metadata) {
267010
267025
  const name = metadata.apiConnection.name;
@@ -267905,6 +267920,52 @@ async function getSchemasForMySQL(connection) {
267905
267920
  }
267906
267921
  ];
267907
267922
  }
267923
+ var SNOWFLAKE_SYSTEM_DATABASES = new Set([
267924
+ "SNOWFLAKE",
267925
+ "SNOWFLAKE_SAMPLE_DATA"
267926
+ ]);
267927
+ async function listSnowflakeSchemasInDatabase(connection, malloyConnection, database, schema) {
267928
+ const filters = [
267929
+ `CATALOG_NAME = '${sqlLiteral(database, connection.type)}'`
267930
+ ];
267931
+ if (schema) {
267932
+ filters.push(`SCHEMA_NAME = '${sqlLiteral(schema, connection.type)}'`);
267933
+ }
267934
+ assertSafeSqlIdentifier(database, "database name");
267935
+ const result = await runIntrospectionSQL(malloyConnection, `SELECT CATALOG_NAME, SCHEMA_NAME, SCHEMA_OWNER FROM ${database}.INFORMATION_SCHEMA.SCHEMATA WHERE ${filters.join(" AND ")} ORDER BY SCHEMA_NAME`);
267936
+ return standardizeRunSQLResult2(result).map((row) => {
267937
+ const r = row;
267938
+ return {
267939
+ catalogName: String(r.CATALOG_NAME ?? r.catalog_name ?? ""),
267940
+ schemaName: String(r.SCHEMA_NAME ?? r.schema_name ?? ""),
267941
+ owner: String(r.SCHEMA_OWNER ?? r.schema_owner ?? "")
267942
+ };
267943
+ });
267944
+ }
267945
+ var SNOWFLAKE_SHOW_ROW_LIMIT = 1e4;
267946
+ async function listSnowflakeSchemasInAccount(malloyConnection) {
267947
+ const result = await runIntrospectionSQL(malloyConnection, `SHOW SCHEMAS IN ACCOUNT LIMIT ${SNOWFLAKE_SHOW_ROW_LIMIT}`);
267948
+ const returnedRows = standardizeRunSQLResult2(result);
267949
+ if (returnedRows.length >= SNOWFLAKE_SHOW_ROW_LIMIT) {
267950
+ logger.warn("Snowflake account-wide schema listing hit the SHOW row limit; the schema list is incomplete and some tables will not be discoverable", {
267951
+ rowLimit: SNOWFLAKE_SHOW_ROW_LIMIT,
267952
+ returnedRows: returnedRows.length
267953
+ });
267954
+ }
267955
+ const parsed = returnedRows.map((row) => {
267956
+ const r = row;
267957
+ return {
267958
+ catalogName: String(r.database_name ?? r.DATABASE_NAME ?? ""),
267959
+ schemaName: String(r.name ?? r.NAME ?? ""),
267960
+ owner: String(r.owner ?? r.OWNER ?? "")
267961
+ };
267962
+ });
267963
+ const usable = parsed.filter((r) => r.catalogName && r.schemaName);
267964
+ if (usable.length < parsed.length) {
267965
+ logger.warn("Dropped Snowflake schema rows missing a database or schema name; the schema list is incomplete", { dropped: parsed.length - usable.length, returned: parsed.length });
267966
+ }
267967
+ return usable.sort((a, b) => a.catalogName.localeCompare(b.catalogName) || a.schemaName.localeCompare(b.schemaName));
267968
+ }
267908
267969
  async function getSchemasForSnowflake(connection, malloyConnection) {
267909
267970
  if (!connection.snowflakeConnection) {
267910
267971
  throw new Error("Snowflake connection is required");
@@ -267912,28 +267973,15 @@ async function getSchemasForSnowflake(connection, malloyConnection) {
267912
267973
  try {
267913
267974
  const database = connection.snowflakeConnection.database;
267914
267975
  const schema = connection.snowflakeConnection.schema;
267915
- const filters = [];
267916
- if (database) {
267917
- filters.push(`CATALOG_NAME = '${sqlLiteral(database, connection.type)}'`);
267918
- }
267919
- if (schema) {
267920
- filters.push(`SCHEMA_NAME = '${sqlLiteral(schema, connection.type)}'`);
267921
- }
267922
- const whereClause = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
267923
- const result = await runIntrospectionSQL(malloyConnection, `SELECT CATALOG_NAME, SCHEMA_NAME, SCHEMA_OWNER FROM ${database ? `${database}.` : ""}INFORMATION_SCHEMA.SCHEMATA ${whereClause} ORDER BY SCHEMA_NAME`);
267924
- const rows = standardizeRunSQLResult2(result);
267925
- return rows.map((row) => {
267926
- const typedRow = row;
267927
- const catalogName = String(typedRow.CATALOG_NAME ?? typedRow.catalog_name ?? "");
267928
- const schemaName = String(typedRow.SCHEMA_NAME ?? typedRow.schema_name ?? "");
267929
- const owner = String(typedRow.SCHEMA_OWNER ?? typedRow.schema_owner ?? "");
267930
- return {
267931
- name: `${catalogName}.${schemaName}`,
267932
- isHidden: ["SNOWFLAKE", ""].includes(owner) || schemaName === "INFORMATION_SCHEMA",
267933
- isDefault: schema ? schemaName === schema : false
267934
- };
267935
- });
267976
+ const rows = database ? await listSnowflakeSchemasInDatabase(connection, malloyConnection, database, schema) : await listSnowflakeSchemasInAccount(malloyConnection);
267977
+ return rows.map(({ catalogName, schemaName, owner }) => ({
267978
+ name: `${catalogName}.${schemaName}`,
267979
+ isHidden: owner === "SNOWFLAKE" || Boolean(database) && owner === "" || schemaName === "INFORMATION_SCHEMA" || !database && SNOWFLAKE_SYSTEM_DATABASES.has(catalogName),
267980
+ isDefault: Boolean(database) && Boolean(schema) && schemaName === schema
267981
+ }));
267936
267982
  } catch (error) {
267983
+ if (error instanceof BadRequestError)
267984
+ throw error;
267937
267985
  logger.error(`Error getting schemas for Snowflake connection ${connection.name}`, { error });
267938
267986
  throw new Error(`Failed to get schemas for Snowflake connection ${connection.name}: ${error.message}`);
267939
267987
  }
@@ -269317,7 +269365,7 @@ class QueryController {
269317
269365
  constructor(environmentStore) {
269318
269366
  this.environmentStore = environmentStore;
269319
269367
  }
269320
- async getQuery(environmentName, packageName, modelPath, sourceName, queryName, query, compactJson = false, filterParams, bypassFilters, givens, metadata) {
269368
+ async getQuery(environmentName, packageName, modelPath, sourceName, queryName, query, compactJson = false, filterParams, bypassFilters, givens, metadata, bypassAuthorize) {
269321
269369
  let requestMetadata;
269322
269370
  let queryClass;
269323
269371
  try {
@@ -269357,7 +269405,7 @@ class QueryController {
269357
269405
  return null;
269358
269406
  }
269359
269407
  }
269360
- }, compactJson ? "compact" : "full"), getQueryTimeoutMs());
269408
+ }, compactJson ? "compact" : "full", bypassAuthorize), getQueryTimeoutMs());
269361
269409
  const renderLogs = import_render_validator.validateRenderTags(result);
269362
269410
  return {
269363
269411
  result: serializedResult,
@@ -277169,12 +277217,19 @@ function unwrapQuotedExpression(body) {
277169
277217
 
277170
277218
  // src/authorize_metrics.ts
277171
277219
  var guardRejectionCounter = null;
277220
+ var bypassCounter = null;
277172
277221
  function recordAuthorizeGuardRejection(field) {
277173
277222
  guardRejectionCounter ??= publisherMeter().createCounter("publisher_authorize_guard_rejected_total", {
277174
277223
  description: "Requests rejected with 400 for declaring an `#(authorize)` annotation in caller-submitted Malloy text. Label: field ('query'|'source_name'|'query_name'|'compile_source')."
277175
277224
  });
277176
277225
  guardRejectionCounter.add(1, { field });
277177
277226
  }
277227
+ function recordAuthorizeBypass(entryPoint) {
277228
+ bypassCounter ??= publisherMeter().createCounter("publisher_authorize_bypass_total", {
277229
+ description: "Gate evaluations skipped because the request carried an authorize bypass (private data-management path). Label: entry_point ('source'|'runnable'). Any nonzero value on a path that should not use the bypass is a finding — see the paired `authorize bypass` audit log line for org/package/model/source."
277230
+ });
277231
+ bypassCounter.add(1, { entry_point: entryPoint });
277232
+ }
277178
277233
 
277179
277234
  // src/service/environment.ts
277180
277235
  init_logger();
@@ -278733,7 +278788,11 @@ class Model {
278733
278788
  }
278734
278789
  return this.fileLevelAuthorize;
278735
278790
  }
278736
- async assertAuthorized(sourceName, givens) {
278791
+ async assertAuthorized(sourceName, givens, bypassAuthorize = false) {
278792
+ if (bypassAuthorize) {
278793
+ this.noteAuthorizeBypass("source", sourceName);
278794
+ return;
278795
+ }
278737
278796
  const gates = sourceName ? this.entryPointGatesBySource.get(sourceName) : undefined;
278738
278797
  if (gates) {
278739
278798
  for (const { label, exprs, selfContained, ambientPrefix } of gates) {
@@ -278743,6 +278802,15 @@ class Model {
278743
278802
  }
278744
278803
  await this.assertAuthorizedExprs(sourceName ?? "(query)", this.effectiveAuthorizeFor(sourceName), givens);
278745
278804
  }
278805
+ noteAuthorizeBypass(entryPoint, sourceName) {
278806
+ recordAuthorizeBypass(entryPoint);
278807
+ logger.info("authorize bypass", {
278808
+ entryPoint,
278809
+ sourceName: sourceName ?? "(query)",
278810
+ modelPath: this.modelPath,
278811
+ packageName: this.packageName
278812
+ });
278813
+ }
278746
278814
  async assertAuthorizedExprs(label, exprs, givens, selfContainedFirst = false, ambientPrefix = 0) {
278747
278815
  if (exprs.length === 0)
278748
278816
  return;
@@ -278765,7 +278833,11 @@ class Model {
278765
278833
  if (!passed)
278766
278834
  deny();
278767
278835
  }
278768
- async assertAuthorizedForAllSources(runnable, givens) {
278836
+ async assertAuthorizedForAllSources(runnable, givens, bypassAuthorize = false) {
278837
+ if (bypassAuthorize) {
278838
+ this.noteAuthorizeBypass("runnable", await this.resolveAuthorizeSourceFromRunnable(runnable));
278839
+ return;
278840
+ }
278769
278841
  const ownSourceName = await this.resolveAuthorizeSourceFromRunnable(runnable);
278770
278842
  await this.assertAuthorized(ownSourceName, givens);
278771
278843
  const { struct, modelDef, compositeResolvedSourceDef } = await this.resolveRunTargetStruct(runnable);
@@ -279342,7 +279414,7 @@ class Model {
279342
279414
  return { ...b, schema, refinements };
279343
279415
  }).filter((b) => b.schema.length > 0);
279344
279416
  }
279345
- async getQueryResults(sourceName, queryName, query, filterParams, bypassFilters, givens, abortSignal, queryMetadataInput, responseShape = "full") {
279417
+ async getQueryResults(sourceName, queryName, query, filterParams, bypassFilters, givens, abortSignal, queryMetadataInput, responseShape = "full", bypassAuthorize = false) {
279346
279418
  const startTime = performance.now();
279347
279419
  if (this.compilationError) {
279348
279420
  if (this.compilationError instanceof MalloyError2 || this.compilationError instanceof ModelCompilationError) {
@@ -279361,7 +279433,7 @@ class Model {
279361
279433
  const surfaceName = extractRunTargetSourceName(query);
279362
279434
  const earlySource = sourceName || (queryName ? this.queries?.find((q) => q.name === queryName)?.sourceName : undefined) || (surfaceName && !this.sources?.some((s) => s.name === surfaceName) ? this.queries?.find((q) => q.name === surfaceName)?.sourceName : undefined) || surfaceName;
279363
279435
  if (earlySource) {
279364
- await this.assertAuthorized(earlySource, givens ?? {});
279436
+ await this.assertAuthorized(earlySource, givens ?? {}, bypassAuthorize);
279365
279437
  }
279366
279438
  try {
279367
279439
  for (const [field, callerText] of [
@@ -279456,7 +279528,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier(sourceName)} -> ` : ""}${quoteMallo
279456
279528
  if (boundary === "deferred") {
279457
279529
  this.assertQueryBoundaryCompiled(compiledSource, query);
279458
279530
  }
279459
- await this.assertAuthorizedForAllSources(runnable, givens ?? {});
279531
+ await this.assertAuthorizedForAllSources(runnable, givens ?? {}, bypassAuthorize);
279460
279532
  const maxRows = getMaxQueryRows();
279461
279533
  const maxBytes = getMaxResponseBytes();
279462
279534
  const buildManifest = this.resolveFreshBuildManifest();
@@ -284408,6 +284480,13 @@ class WatchModeController {
284408
284480
  init_errors();
284409
284481
  init_logger();
284410
284482
 
284483
+ // src/authorize_bypass_header.ts
284484
+ var BYPASS_AUTHORIZE_HEADER = "x-publisher-bypass-authorize";
284485
+ var readBypassAuthorize = (req) => {
284486
+ const raw = req.headers[BYPASS_AUTHORIZE_HEADER];
284487
+ return typeof raw === "string" && raw.trim().toLowerCase() === "true" ? true : undefined;
284488
+ };
284489
+
284411
284490
  // src/filter_deprecation.ts
284412
284491
  var setFilterDeprecationHeaders = (res, options) => {
284413
284492
  const hasFilterParams = options.filterParams !== undefined && options.filterParams !== null && !(typeof options.filterParams === "object" && !Array.isArray(options.filterParams) && Object.keys(options.filterParams).length === 0);
@@ -295580,7 +295659,7 @@ app.post(`${API_PREFIX2}/environments/:environmentName/packages/:packageName/mod
295580
295659
  queryMetadata: req.body?.queryMetadata,
295581
295660
  queryClass: req.body?.queryClass,
295582
295661
  versionId: req.body?.versionId
295583
- });
295662
+ }, readBypassAuthorize(req));
295584
295663
  setFilterDeprecationHeaders(res, {
295585
295664
  filterParams: req.body.filterParams ?? req.body.sourceFilters,
295586
295665
  bypassFilters: req.body.bypassFilters === true ? true : undefined
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.241",
4
+ "version": "0.0.243",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"