Skip to content

Commit 8ed0b8e

Browse files
Move REST catalog credentials to USER MAPPING, add catalogs.conf
Until now an iceberg_catalog server carried its own client_id / client_secret on the SERVER row. That gave every role on the cluster the same credentials and forced operators to recreate the server when secrets rotated. Move credentials to per-user state and add a platform-provided file as a middle layer. Credential resolution (lowest to highest priority): 1. pg_lake_iceberg.rest_catalog_* GUC defaults 2. iceberg_catalog SERVER options (non-secret only) 3. $PGDATA/catalogs.conf (user-created servers only) 4. pg_user_mapping options (user-created servers only) The built-in pg_lake_rest_catalog (catalog='rest') stops after step 2 on purpose: its credentials live exclusively in the GUCs so the built-in stays a single, global, instance-wide configuration with no hidden per-user view. CREATE/ALTER USER MAPPING is rejected against all three built-in long names; catalogs.conf is also ignored for the built-in 'rest' catalog. Notable pieces: * iceberg_catalog_option_descs[] now carries a CATALOG_OPT_CTX_* bitmask per option. The same table drives the validator, the per-context "Valid options are: ..." hint, and the option->struct applier. client_id / client_secret are USER MAPPING-only; scope is accepted on both, with the USER MAPPING value winning because it is applied last during resolution. * RestCatalogOptions gains a umid field; the token cache key is now (serverOid, umid) so different SET ROLEs in the same backend each get their own user mapping's credentials. A USERMAPPINGOID syscache callback invalidates cached tokens on CREATE/ALTER/DROP USER MAPPING. * ValidateRestCatalogOptions now performs an early auth-type-aware credentials check at resolution time: - client_secret is always required - client_id is required unless rest_auth_type='horizon' Missing credentials surface as "no credentials found for REST catalog ..." with ERRCODE_FDW_OPTION_NAME_NOT_FOUND. The per-field checks inside FetchRestCatalogAccessToken are kept as defense in depth and now carry the same errcode. * New ProcessUtility handler RedactRestCatalogUserMappingSecrets scrubs client_id / client_secret from queryString in place on CREATE/ALTER USER MAPPING for any iceberg_catalog server (built-in long names included). Handles plain '', E'', and U&'' literal forms with their escape rules. Registered after the DDL validator so it runs first (the handler list is prepend-LIFO), ensuring the failing built-in-server path never leaks secrets into the ereport context. DDL itself reads option values from DefElem->arg, so pg_user_mapping still stores plaintext credentials -- only the query string surfaces (pg_stat_statements, log_min_duration_statement, ereport context) see the redacted form. * New PGC_SIGHUP GUC pg_lake_iceberg.catalogs_conf_path lets operators point at an absolute path; the default is the relative 'catalogs.conf' resolved against DataDir. Tests: * test_iceberg_catalog_server.py picks up the user-mapping DDL surface (per-context option lists and hints, valid/invalid options on each side, FOR CURRENT_USER with all three options, per-role mappings on the same server, dependency-driven rejections, built-in long-name blocks), the redaction handler (CREATE/ALTER, '' escape, E'' escape, scope preservation, non-iceberg FDW skip, built-in-rejection-after-redaction, plaintext storage preservation), and credential resolution via catalogs.conf (file-only success, USER MAPPING wins over the file, no-credentials-anywhere error, scope from the file, absolute catalogs_conf_path, built-in ignores the file). * test_modify_iceberg_rest_table.py: client_id / client_secret are dropped from the server-option-overrides-GUC parametrization (they no longer belong on SERVER), and a new parametrized test_user_mapping_credential_overrides_guc covers the same resolution-order direction with USER MAPPING in step 4. * test_writable_iceberg_common.py: the shared fixture for the writable user-created REST server now puts credentials on a PUBLIC user mapping, and drops the server with CASCADE to sweep up the mapping. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: sfc-gh-npuka <naisila.puka@snowflake.com>
1 parent 87dbbc1 commit 8ed0b8e

7 files changed

Lines changed: 2317 additions & 165 deletions

File tree

pg_lake_iceberg/include/pg_lake/rest_catalog/rest_catalog.h

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,37 @@ extern char *RestCatalogClientSecret;
3434
extern char *RestCatalogScope;
3535
extern int RestCatalogAuthType;
3636
extern bool RestCatalogEnableVendedCredentials;
37+
extern char *CatalogsConfPath;
3738

3839
/*
3940
* Resolved REST catalog connection options. All REST catalogs --
4041
* built-in ('rest') and user-created (CREATE SERVER ... FOREIGN DATA
4142
* WRAPPER iceberg_catalog) -- are backed by a real pg_foreign_server
42-
* row; ApplyGUCDefaults populates the defaults, ApplyServerOptionOverrides
43-
* layers on any per-server options.
43+
* row.
4444
*
45-
* The canonical identity of a catalog is `serverOid` (the OID of the
46-
* iceberg_catalog server row). Use it for in-memory equality, token
47-
* cache keys, and syscache-driven invalidation. `catalog` stores the
48-
* user-visible short name (e.g. 'rest', 'my_polaris') purely for error
49-
* messages.
45+
* Resolution order, lowest to highest priority:
46+
* 1. GUC defaults (ApplyGUCDefaults)
47+
* 2. Server options (ApplyServerOptionOverrides)
48+
* 3. $PGDATA/catalogs.conf credentials (user-created servers only)
49+
* 4. pg_user_mapping options (user-created servers only)
50+
*
51+
* In-memory identity is the pair (`serverOid`, `umid`):
52+
* - serverOid is the iceberg_catalog server's OID.
53+
* - umid is the OID of the pg_user_mapping row that contributed the
54+
* credentials, or InvalidOid when no user mapping was used (built-in
55+
* pg_lake_rest_catalog, or a user-created server whose credentials
56+
* came entirely from catalogs.conf / GUCs).
57+
*
58+
* `catalog` is the user-visible short name (e.g. 'rest', 'my_polaris')
59+
* kept purely for error messages.
5060
*/
5161
typedef struct RestCatalogOptions
5262
{
5363
Oid serverOid; /* iceberg_catalog server OID; canonical
5464
* identity, never InvalidOid for resolved
5565
* opts */
66+
Oid umid; /* pg_user_mapping row OID that supplied
67+
* credentials, or InvalidOid if none */
5668
char *catalog; /* short user-facing name; used in error
5769
* messages, never for equality */
5870
char *host;
@@ -138,3 +150,11 @@ extern PGDLLEXPORT RestCatalogRequest * GetRemoveSnapshotCatalogRequest(List *re
138150

139151
/* ProcessUtility handler for iceberg_catalog server DDL validation */
140152
extern PGDLLEXPORT bool ValidateIcebergCatalogServerDDL(ProcessUtilityParams * processUtilityParams, void *arg);
153+
154+
/*
155+
* ProcessUtility handler that scrubs client_id / client_secret out of
156+
* queryString in CREATE/ALTER USER MAPPING for iceberg_catalog
157+
* servers, in place. Register after ValidateIcebergCatalogServerDDL
158+
* so it runs first (the handler list is a prepend-LIFO).
159+
*/
160+
extern PGDLLEXPORT bool RedactRestCatalogUserMappingSecrets(ProcessUtilityParams * processUtilityParams, void *arg);

pg_lake_iceberg/pg_lake_iceberg--3.3--3.4.sql

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,36 @@
55
* configurations via CREATE SERVER so that users are not limited to a
66
* single global REST catalog configured through GUC settings.
77
*
8-
* Example:
8+
* Server options (non-secret): rest_endpoint, rest_auth_type,
9+
* oauth_endpoint, scope, enable_vended_credentials, location_prefix,
10+
* catalog_name.
11+
* User mapping options (credentials): client_id, client_secret, scope.
12+
*
13+
* scope is accepted in both server and user mapping; user mapping wins.
14+
*
15+
* Credential resolution order:
16+
* 1. CREATE USER MAPPING for the current user
17+
* 2. $PGDATA/catalogs.conf (platform-provided)
18+
* 3. GUC variables (backward compatibility)
19+
*
20+
* User-defined catalog example:
921
* CREATE SERVER my_polaris TYPE 'rest'
1022
* FOREIGN DATA WRAPPER iceberg_catalog
11-
* OPTIONS (rest_endpoint 'http://polaris:8181',
12-
* rest_auth_type 'default',
13-
* client_id '...',
14-
* client_secret '...');
23+
* OPTIONS (rest_endpoint 'https://polaris.example.com');
24+
*
25+
* CREATE USER MAPPING FOR user1 SERVER my_polaris
26+
* OPTIONS (client_id '...', client_secret '...');
1527
*
1628
* CREATE TABLE t (a int) USING iceberg WITH (catalog = 'my_polaris');
29+
*
30+
* Platform-provided catalog example:
31+
* CREATE SERVER horizon TYPE 'rest'
32+
* FOREIGN DATA WRAPPER iceberg_catalog
33+
* OPTIONS (rest_endpoint 'https://horizon.example.com');
34+
*
35+
* -- Credentials in $PGDATA/catalogs.conf:
36+
* -- horizon.client_id = 'platform_id'
37+
* -- horizon.client_secret = 'platform_secret'
1738
*/
1839
CREATE FUNCTION lake_iceberg.iceberg_catalog_validator(text[], oid)
1940
RETURNS void

pg_lake_iceberg/src/init.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,16 @@ _PG_init(void)
321321
GUC_SUPERUSER_ONLY | GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE,
322322
NULL, NULL, NULL);
323323

324+
DefineCustomStringVariable("pg_lake_iceberg.catalogs_conf_path",
325+
gettext_noop("Path to the catalog credentials file. "
326+
"Defaults to $PGDATA/catalogs.conf."),
327+
NULL,
328+
&CatalogsConfPath,
329+
"catalogs.conf",
330+
PGC_SIGHUP,
331+
GUC_SUPERUSER_ONLY,
332+
NULL, NULL, NULL);
333+
324334
DefineCustomBoolVariable("pg_lake_iceberg.unsupported_numeric_as_double",
325335
gettext_noop("When enabled, numeric columns that cannot be represented "
326336
"as Iceberg decimals (unbounded or precision > 38) are "
@@ -335,6 +345,14 @@ _PG_init(void)
335345
AvroInit();
336346

337347
RegisterUtilityStatementHandler(ValidateIcebergCatalogServerDDL, NULL);
348+
349+
/*
350+
* Register last so it runs first: RegisterUtilityStatementHandler
351+
* prepends to a linked list. Redaction must precede the validator above
352+
* so that the failing built-in-server path never leaks client_id /
353+
* client_secret in its error context.
354+
*/
355+
RegisterUtilityStatementHandler(RedactRestCatalogUserMappingSecrets, NULL);
338356
}
339357

340358

0 commit comments

Comments
 (0)