Skip to content

Commit 36b145f

Browse files
committed
Address review
Rename ScrubIcebergUserMappingHandler to RedactRestCatalogUserMappingSecrets and move its registration from pg_lake_iceberg to pg_lake_table, alongside BlockDDLOnExtensionCatalogs. Reorder credential resolution in GetRestCatalogConnectionFromServer so the code reads in ascending priority: GUCs (defaults) → catalogs.conf → user mapping. Previously catalogs.conf was checked after user mapping with NULL guards to avoid overriding it. Guard catalogs.conf parser against malformed entries like "rest." (dot with no key after it) by checking item->name[serverNameLen + 1] != '\0'. Add test for catalogs.conf on the extension-owned rest server. Add DDL-level test for per-user scope isolation: two roles with different scopes on the same server, verified via pg_user_mapping. Signed-off-by: sfc-gh-npuka <naisila.puka@snowflake.com>
1 parent 665a0f5 commit 36b145f

5 files changed

Lines changed: 133 additions & 28 deletions

File tree

pg_lake_iceberg/include/pg_lake/rest_catalog/rest_catalog.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ extern PGDLLEXPORT RestCatalogRequest * GetRemoveSnapshotCatalogRequest(List *re
127127
/* protects extension-owned catalog servers */
128128
extern PGDLLEXPORT bool BlockDDLOnExtensionCatalogs(ProcessUtilityParams *processUtilityParams, void *arg);
129129
/* scrubs user mapping secrets in-place */
130-
extern PGDLLEXPORT bool ScrubIcebergUserMappingHandler(ProcessUtilityParams *processUtilityParams, void *arg);
130+
extern PGDLLEXPORT bool RedactRestCatalogUserMappingSecrets(ProcessUtilityParams *processUtilityParams, void *arg);

pg_lake_iceberg/src/init.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,6 @@ _PG_init(void)
330330
NULL, NULL, NULL);
331331

332332
AvroInit();
333-
334-
RegisterUtilityStatementHandler(ScrubIcebergUserMappingHandler, NULL);
335333
}
336334

337335

pg_lake_iceberg/src/rest_catalog/rest_catalog.c

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,14 @@ ScrubUserMappingSecrets(const char *queryString, List *options)
295295

296296

297297
/*
298-
* ScrubIcebergUserMappingHandler is a ProcessUtility handler registered via
299-
* pg_lake_engine's RegisterUtilityStatementHandler. When it detects a
300-
* CREATE/ALTER USER MAPPING targeting an iceberg_catalog server, it scrubs
301-
* secret values in the queryString in-place and returns false so normal
298+
* RedactRestCatalogUserMappingSecrets is a ProcessUtility handler that detects
299+
* CREATE/ALTER USER MAPPING targeting an iceberg_catalog server and scrubs
300+
* secret values in the queryString in-place. Returns false so normal
302301
* processing continues.
303302
*/
304303
bool
305-
ScrubIcebergUserMappingHandler(ProcessUtilityParams *processUtilityParams,
306-
void *arg)
304+
RedactRestCatalogUserMappingSecrets(ProcessUtilityParams *processUtilityParams,
305+
void *arg)
307306
{
308307
Node *parsetree = processUtilityParams->plannedStmt->utilityStmt;
309308
const char *serverName = NULL;
@@ -564,7 +563,8 @@ ReadCatalogsConfCredentials(const char *serverName,
564563
* The dot separates the server name from the property name.
565564
*/
566565
if (strncmp(item->name, serverName, serverNameLen) != 0 ||
567-
item->name[serverNameLen] != '.')
566+
item->name[serverNameLen] != '.' ||
567+
item->name[serverNameLen + 1] == '\0')
568568
continue;
569569

570570
key = item->name + serverNameLen + 1;
@@ -663,8 +663,25 @@ GetRestCatalogConnectionFromServer(const char *serverName)
663663
/*
664664
* Phase 2: Resolve credentials and scope.
665665
*
666-
* Priority: user mapping > catalogs.conf > GUC fallback (set above).
666+
* Priority (ascending): GUC (set above) < catalogs.conf < user mapping.
667667
*/
668+
char *confClientId = NULL;
669+
char *confClientSecret = NULL;
670+
char *confScope = NULL;
671+
672+
if (ReadCatalogsConfCredentials(serverName,
673+
&confClientId, &confClientSecret,
674+
&confScope))
675+
{
676+
if (confClientId != NULL)
677+
conn->clientId = confClientId;
678+
if (confClientSecret != NULL)
679+
conn->clientSecret = confClientSecret;
680+
if (confScope != NULL)
681+
conn->scope = confScope;
682+
}
683+
684+
/* User mapping has highest priority — overrides everything above */
668685
List *umOptions = LookupUserMappingOptions(server->serverid);
669686

670687
foreach(lc, umOptions)
@@ -679,23 +696,6 @@ GetRestCatalogConnectionFromServer(const char *serverName)
679696
conn->scope = pstrdup(defGetString(def));
680697
}
681698

682-
/* catalogs.conf overrides GUCs but not user mapping */
683-
char *confClientId = NULL;
684-
char *confClientSecret = NULL;
685-
char *confScope = NULL;
686-
687-
if (ReadCatalogsConfCredentials(serverName,
688-
&confClientId, &confClientSecret,
689-
&confScope))
690-
{
691-
if (conn->clientId == NULL && confClientId != NULL)
692-
conn->clientId = confClientId;
693-
if (conn->clientSecret == NULL && confClientSecret != NULL)
694-
conn->clientSecret = confClientSecret;
695-
if (conn->scope == NULL && confScope != NULL)
696-
conn->scope = confScope;
697-
}
698-
699699
if (conn->clientId == NULL || conn->clientSecret == NULL)
700700
ereport(ERROR,
701701
(errcode(ERRCODE_FDW_OPTION_NAME_NOT_FOUND),

pg_lake_table/src/init.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,10 @@ _PG_init(void)
351351

352352
MarkGUCPrefixReserved(PG_LAKE_TABLE);
353353

354+
/* iceberg_catalog server / user mapping guards */
354355
RegisterUtilityStatementHandler(BlockDDLOnExtensionCatalogs, NULL);
356+
RegisterUtilityStatementHandler(RedactRestCatalogUserMappingSecrets, NULL);
357+
355358
RegisterUtilityStatementHandler(ProcessVacuumPgLakeTable, NULL);
356359
RegisterUtilityStatementHandler(ProcessCreatePgLakeTable, NULL);
357360
RegisterUtilityStatementHandler(ProcessCreateAsSelectPgLakeTable, NULL);

pg_lake_table/tests/pytests/test_iceberg_catalog_server.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,59 @@ def test_reject_server_options_in_user_mapping(superuser_conn, extension):
290290
superuser_conn.rollback()
291291

292292

293+
def test_per_user_scope_in_user_mapping(superuser_conn, extension):
294+
"""Different users can have different scopes on the same server."""
295+
run_command(
296+
"""
297+
CREATE SERVER test_scope_srv TYPE 'rest'
298+
FOREIGN DATA WRAPPER iceberg_catalog
299+
OPTIONS (rest_endpoint 'http://localhost:8181')
300+
""",
301+
superuser_conn,
302+
)
303+
run_command("CREATE ROLE analyst LOGIN", superuser_conn)
304+
run_command("CREATE ROLE admin_user LOGIN", superuser_conn)
305+
run_command(
306+
"""
307+
CREATE USER MAPPING FOR analyst SERVER test_scope_srv
308+
OPTIONS (client_id 'a-id', client_secret 'a-secret',
309+
scope 'PRINCIPAL_ROLE:READER')
310+
""",
311+
superuser_conn,
312+
)
313+
run_command(
314+
"""
315+
CREATE USER MAPPING FOR admin_user SERVER test_scope_srv
316+
OPTIONS (client_id 'b-id', client_secret 'b-secret',
317+
scope 'PRINCIPAL_ROLE:ALL')
318+
""",
319+
superuser_conn,
320+
)
321+
322+
result = run_query(
323+
"""
324+
SELECT u.rolname, array_to_string(um.umoptions, ', ') AS opts
325+
FROM pg_user_mapping um
326+
JOIN pg_roles u ON u.oid = um.umuser
327+
WHERE um.umserver = (SELECT oid FROM pg_foreign_server WHERE srvname = 'test_scope_srv')
328+
ORDER BY u.rolname
329+
""",
330+
superuser_conn,
331+
)
332+
assert len(result) == 2
333+
assert "PRINCIPAL_ROLE:ALL" in result[0]["opts"] # admin_user
334+
assert "PRINCIPAL_ROLE:READER" in result[1]["opts"] # analyst
335+
336+
run_command("DROP USER MAPPING FOR analyst SERVER test_scope_srv", superuser_conn)
337+
run_command(
338+
"DROP USER MAPPING FOR admin_user SERVER test_scope_srv", superuser_conn
339+
)
340+
run_command("DROP SERVER test_scope_srv", superuser_conn)
341+
run_command("DROP ROLE analyst", superuser_conn)
342+
run_command("DROP ROLE admin_user", superuser_conn)
343+
superuser_conn.rollback()
344+
345+
293346
# ── CREATE FOREIGN TABLE on iceberg_catalog servers is blocked ──────────────
294347

295348

@@ -748,6 +801,57 @@ def test_scope_from_catalogs_conf(
748801
superuser_conn.commit()
749802

750803

804+
def test_credentials_from_catalogs_conf_for_rest_server(
805+
pg_conn,
806+
superuser_conn,
807+
s3,
808+
extension,
809+
with_default_location,
810+
catalogs_conf,
811+
):
812+
"""catalogs.conf credentials for the extension-owned 'rest' server"""
813+
814+
# Clearing the GUCs shouldn't matter when catalogs.conf is set (it should
815+
# take priority), but there is currently no way to verify that credentials
816+
# are actually coming from catalogs.conf rather than GUC fallback.
817+
run_command("SET pg_lake_iceberg.rest_catalog_client_id TO ''", superuser_conn)
818+
run_command("SET pg_lake_iceberg.rest_catalog_client_secret TO ''", superuser_conn)
819+
820+
# Without catalogs.conf, credentials should be missing
821+
err = run_command(
822+
"""
823+
CREATE TABLE test_rest_conf_tbl ()
824+
USING iceberg
825+
WITH (catalog = 'rest', read_only = 'true',
826+
catalog_namespace = 'ns', catalog_table_name = 'tbl')
827+
""",
828+
pg_conn,
829+
raise_error=False,
830+
)
831+
assert err is not None
832+
assert "no credentials found" in str(err)
833+
pg_conn.rollback()
834+
835+
# Now write catalogs.conf — credentials should resolve
836+
catalogs_conf(
837+
"rest.client_id = 'platform-id'\n" "rest.client_secret = 'platform-secret'\n"
838+
)
839+
840+
err = run_command(
841+
"""
842+
CREATE TABLE test_rest_conf_tbl ()
843+
USING iceberg
844+
WITH (catalog = 'rest', read_only = 'true',
845+
catalog_namespace = 'ns', catalog_table_name = 'tbl')
846+
""",
847+
pg_conn,
848+
raise_error=False,
849+
)
850+
assert err is not None
851+
assert "no credentials found" not in str(err)
852+
pg_conn.rollback()
853+
854+
751855
# ── Backward compatibility ─────────────────────────────────────────────────
752856

753857

0 commit comments

Comments
 (0)