Skip to content

Commit 6802bf9

Browse files
committed
Address review part 1
- Block CREATE FOREIGN TABLE on iceberg_catalog servers. The iceberg_catalog FDW has no handler, so foreign tables created on it would fail at query time with "has no handler". The check is added to ErrorUnsupportedCreatePgLakeTableHandler in pg_lake_table, which already runs first for all CREATE FOREIGN TABLE statements. - Block ALTER SERVER ... OWNER TO on extension-owned catalog servers (postgres, object_store, rest). - Move ICEBERG_CATALOG_FDW_NAME from rest_catalog.h to catalog_type.h alongside the other catalog name constants, since it is referenced by both pg_lake_iceberg and pg_lake_table. - Rename rest_auth_type value "default" to "oauth2" to better describe the standard OAuth2 client_credentials grant with Basic auth. "default" value is also kept. - Rename ProtectExtensionCatalogServersHandler to BlockDDLOnExtensionCatalogs for clarity. - Move BlockDDLOnExtensionCatalogs registration from pg_lake_iceberg init to pg_lake_table init, where all other ProcessUtility hooks are registered. Signed-off-by: sfc-gh-npuka <naisila.puka@snowflake.com>
1 parent c613b0f commit 6802bf9

7 files changed

Lines changed: 142 additions & 43 deletions

File tree

pg_lake_engine/include/pg_lake/util/catalog_type.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
#pragma once
1919

20+
/* FDW name for iceberg_catalog servers */
21+
#define ICEBERG_CATALOG_FDW_NAME "iceberg_catalog"
22+
2023
/*
2124
* The allowed values for IcebergDefaultCatalog, case insensitive.
2225
*/

pg_lake_iceberg/include/pg_lake/rest_catalog/rest_catalog.h

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
#include "pg_lake/parquet/field.h"
2525
#include "pg_lake/iceberg/api/snapshot.h"
2626

27-
#define REST_CATALOG_AUTH_TYPE_DEFAULT (0)
27+
#define REST_CATALOG_AUTH_TYPE_OAUTH2 (0)
2828
#define REST_CATALOG_AUTH_TYPE_HORIZON (1)
2929

3030
extern PGDLLEXPORT char *RestCatalogHost;
@@ -99,9 +99,6 @@ typedef struct RestCatalogRequest
9999
extern PGDLLEXPORT RestCatalogConnectionInfo * GetRestCatalogConnectionFromServer(const char *serverName);
100100
extern PGDLLEXPORT RestCatalogConnectionInfo * GetRestCatalogConnectionForRelation(Oid relationId);
101101

102-
/* FDW name for iceberg_catalog servers */
103-
#define ICEBERG_CATALOG_FDW_NAME "iceberg_catalog"
104-
105102
extern PGDLLEXPORT void RegisterNamespaceToRestCatalog(RestCatalogConnectionInfo * conn, const char *catalogName, const char *namespaceName);
106103
extern PGDLLEXPORT void StartStageRestCatalogIcebergTableCreate(Oid relationId);
107104
extern PGDLLEXPORT char *FinishStageRestCatalogIcebergTableCreateRestRequest(Oid relationId, DataFileSchema * dataFileSchema, List *partitionSpecs);
@@ -126,4 +123,4 @@ extern PGDLLEXPORT RestCatalogRequest * GetSetPartitionDefaultIdCatalogRequest(O
126123
extern PGDLLEXPORT RestCatalogRequest * GetRemoveSnapshotCatalogRequest(List *removedSnapshotIds, Oid relationId);
127124

128125
/* ProcessUtility handler: protects extension-owned catalog servers */
129-
extern PGDLLEXPORT bool ProtectExtensionCatalogServersHandler(ProcessUtilityParams *processUtilityParams, void *arg);
126+
extern PGDLLEXPORT bool BlockDDLOnExtensionCatalogs(ProcessUtilityParams *processUtilityParams, void *arg);

pg_lake_iceberg/src/init.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ void _PG_init(void);
5959

6060
/* pg_lake_iceberg.rest_catalog_auth_type */
6161
static const struct config_enum_entry RestCatalogAuthTypeOptions[] = {
62-
{"default", REST_CATALOG_AUTH_TYPE_DEFAULT, false},
62+
{"oauth2", REST_CATALOG_AUTH_TYPE_OAUTH2, false},
63+
{"default", REST_CATALOG_AUTH_TYPE_OAUTH2, false},
6364
{"horizon", REST_CATALOG_AUTH_TYPE_HORIZON, false},
6465
{NULL, 0, false},
6566
};
@@ -256,7 +257,7 @@ _PG_init(void)
256257
gettext_noop("Determines the format for the initial OAuth token requests."),
257258
NULL,
258259
&RestCatalogAuthType,
259-
REST_CATALOG_AUTH_TYPE_DEFAULT,
260+
REST_CATALOG_AUTH_TYPE_OAUTH2,
260261
RestCatalogAuthTypeOptions,
261262
PGC_SUSET,
262263
GUC_SUPERUSER_ONLY | GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE,
@@ -329,8 +330,6 @@ _PG_init(void)
329330
NULL, NULL, NULL);
330331

331332
AvroInit();
332-
333-
RegisterUtilityStatementHandler(ProtectExtensionCatalogServersHandler, NULL);
334333
}
335334

336335

pg_lake_iceberg/src/rest_catalog/rest_catalog.c

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ char *RestCatalogOauthHostPath = "";
5656
char *RestCatalogClientId = NULL;
5757
char *RestCatalogClientSecret = NULL;
5858
char *RestCatalogScope = "PRINCIPAL_ROLE:ALL";
59-
int RestCatalogAuthType = REST_CATALOG_AUTH_TYPE_DEFAULT;
59+
int RestCatalogAuthType = REST_CATALOG_AUTH_TYPE_OAUTH2;
6060
bool RestCatalogEnableVendedCredentials = true;
6161

6262
/*
@@ -157,11 +157,13 @@ iceberg_catalog_validator(PG_FUNCTION_ARGS)
157157
{
158158
char *authType = defGetString(def);
159159

160-
if (strcmp(authType, "default") != 0 && strcmp(authType, "horizon") != 0)
160+
if (strcmp(authType, "oauth2") != 0 &&
161+
strcmp(authType, "default") != 0 &&
162+
strcmp(authType, "horizon") != 0)
161163
ereport(ERROR,
162164
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
163165
errmsg("invalid rest_auth_type option: \"%s\"", authType),
164-
errhint("Valid values are \"default\" and \"horizon\".")));
166+
errhint("Valid values are \"oauth2\" and \"horizon\".")));
165167
}
166168
else if (strcmp(def->defname, "enable_vended_credentials") == 0)
167169
{
@@ -192,7 +194,7 @@ IsIcebergCatalogServer(const char *serverName)
192194

193195

194196
/*
195-
* ProtectExtensionCatalogServersHandler guards the extension-owned
197+
* BlockDDLOnExtensionCatalogs guards the extension-owned
196198
* iceberg_catalog servers (postgres, object_store, rest) against
197199
* unauthorized DDL.
198200
*
@@ -202,9 +204,10 @@ IsIcebergCatalogServer(const char *serverName)
202204
* - ALTER SERVER on 'rest' is allowed (users may set options).
203205
* - DROP SERVER on 'postgres', 'object_store', or 'rest' is blocked.
204206
* - ALTER ... RENAME on 'postgres', 'object_store', or 'rest' is blocked.
207+
* - ALTER ... OWNER TO on 'postgres', 'object_store', or 'rest' is blocked.
205208
*/
206209
bool
207-
ProtectExtensionCatalogServersHandler(ProcessUtilityParams *processUtilityParams,
210+
BlockDDLOnExtensionCatalogs(ProcessUtilityParams *processUtilityParams,
208211
void *arg)
209212
{
210213
Node *parsetree = processUtilityParams->plannedStmt->utilityStmt;
@@ -286,6 +289,24 @@ ProtectExtensionCatalogServersHandler(ProcessUtilityParams *processUtilityParams
286289
errmsg("cannot rename the extension-owned \"%s\" catalog server",
287290
serverName)));
288291
}
292+
else if (IsA(parsetree, AlterOwnerStmt))
293+
{
294+
AlterOwnerStmt *stmt = (AlterOwnerStmt *) parsetree;
295+
296+
if (stmt->objectType != OBJECT_FOREIGN_SERVER)
297+
return false;
298+
299+
char *serverName = strVal(stmt->object);
300+
301+
if (!IsIcebergCatalogServer(serverName))
302+
return false;
303+
304+
if (IsCatalogOwnedByExtension(serverName))
305+
ereport(ERROR,
306+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
307+
errmsg("cannot change owner of the extension-owned \"%s\" catalog server",
308+
serverName)));
309+
}
289310

290311
return false;
291312
}
@@ -347,7 +368,7 @@ GetRestCatalogConnectionFromServer(const char *serverName)
347368

348369
conn->authType = (strcmp(authType, "horizon") == 0)
349370
? REST_CATALOG_AUTH_TYPE_HORIZON
350-
: REST_CATALOG_AUTH_TYPE_DEFAULT;
371+
: REST_CATALOG_AUTH_TYPE_OAUTH2;
351372
}
352373
else if (strcmp(def->defname, "oauth_endpoint") == 0)
353374
conn->oauthHostPath = defGetString(def);

pg_lake_table/src/ddl/create_table.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ static bool IsJsonOrCSVBackedTable(PgLakeTableType tableType, List *options);
9090
static void ErrorIfUnsupportedColumnTypeForJsonOrCSVTables(List *columnDefList);
9191
static void ErrorIfUsingGeometryWithoutSpatialAnalytics(List *columnDefList);
9292
static void ErrorIfUnsupportedLakeTable(CreateForeignTableStmt *createStmt);
93+
static void ErrorIfCreateForeignTableOnIcebergCatalog(CreateForeignTableStmt *createStmt);
9394
static void ErrorIfWritableTableWithReservedColumnName(List *columnDefList, PgLakeTableType tableType);
9495
static void ErrorIfInvalidFilenameColumn(List *columnDefList);
9596
static bool IsConflictingColumnNameForReadParquet(const char *columnName);
@@ -324,6 +325,9 @@ ErrorIfUsingGeometryWithoutSpatialAnalytics(List *columnDefList)
324325
*
325326
* We check for unsupported features in the table definition, such as unsupported URLs or unsupported
326327
* combinations such as writable tables without column definitions.
328+
*
329+
* Also blocks CREATE FOREIGN TABLE on iceberg_catalog servers, which have no
330+
* handler. Tables should be created via CREATE TABLE ... USING iceberg instead.
327331
*/
328332
bool
329333
ErrorUnsupportedCreatePgLakeTableHandler(ProcessUtilityParams * params, void *arg)
@@ -339,6 +343,8 @@ ErrorUnsupportedCreatePgLakeTableHandler(ProcessUtilityParams * params, void *ar
339343
CreateForeignTableStmt *createStmt =
340344
(CreateForeignTableStmt *) plannedStmt->utilityStmt;
341345

346+
ErrorIfCreateForeignTableOnIcebergCatalog(createStmt);
347+
342348
if (!IsCreateLakeTable(createStmt))
343349
{
344350
/* not a lake table */
@@ -351,6 +357,31 @@ ErrorUnsupportedCreatePgLakeTableHandler(ProcessUtilityParams * params, void *ar
351357
}
352358

353359

360+
/*
361+
* ErrorIfCreateForeignTableOnIcebergCatalog blocks CREATE FOREIGN TABLE
362+
* when the target server uses the iceberg_catalog FDW, which has no handler.
363+
*/
364+
static void
365+
ErrorIfCreateForeignTableOnIcebergCatalog(CreateForeignTableStmt *createStmt)
366+
{
367+
ForeignServer *server =
368+
GetForeignServerByName(createStmt->servername, true);
369+
370+
if (server == NULL)
371+
return;
372+
373+
ForeignDataWrapper *fdw = GetForeignDataWrapper(server->fdwid);
374+
375+
if (strcmp(fdw->fdwname, ICEBERG_CATALOG_FDW_NAME) == 0)
376+
ereport(ERROR,
377+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
378+
errmsg("cannot create foreign tables on iceberg_catalog server \"%s\"",
379+
createStmt->servername),
380+
errhint("Use CREATE TABLE ... USING iceberg WITH (catalog = '%s') instead.",
381+
createStmt->servername)));
382+
}
383+
384+
354385
/*
355386
* ErrorIfUnsupportedLakeTable is a helper function for checking unsupported features
356387
* in CREATE FOREIGN TABLE statements that are pg_lake tables.

pg_lake_table/src/init.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "pg_lake/planner/insert_select.h"
4343
#include "pg_lake/planner/query_pushdown.h"
4444
#include "pg_lake/util/s3_file_utils.h"
45+
#include "pg_lake/rest_catalog/rest_catalog.h"
4546
#include "pg_lake/test/hide_lake_objects.h"
4647
#include "pg_lake/transaction/transaction_hooks.h"
4748
#include "utils/guc.h"
@@ -350,6 +351,7 @@ _PG_init(void)
350351

351352
MarkGUCPrefixReserved(PG_LAKE_TABLE);
352353

354+
RegisterUtilityStatementHandler(BlockDDLOnExtensionCatalogs, NULL);
353355
RegisterUtilityStatementHandler(ProcessVacuumPgLakeTable, NULL);
354356
RegisterUtilityStatementHandler(ProcessCreatePgLakeTable, NULL);
355357
RegisterUtilityStatementHandler(ProcessCreateAsSelectPgLakeTable, NULL);

pg_lake_table/tests/pytests/test_iceberg_catalog_server.py

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_create_rest_server_with_all_options(superuser_conn, extension):
5757
FOREIGN DATA WRAPPER iceberg_catalog
5858
OPTIONS (
5959
rest_endpoint 'http://localhost:8181',
60-
rest_auth_type 'default',
60+
rest_auth_type 'oauth2',
6161
oauth_endpoint 'http://localhost:8181/oauth/tokens',
6262
scope 'PRINCIPAL_ROLE:ALL',
6363
enable_vended_credentials 'true',
@@ -135,12 +135,12 @@ def test_reject_unknown_server_option(superuser_conn, extension):
135135

136136

137137
def test_reject_invalid_auth_type(superuser_conn, extension):
138-
"""Only 'default' and 'horizon' are valid for rest_auth_type."""
138+
"""Only 'oauth2', 'default', and 'horizon' are valid for rest_auth_type."""
139139
err = run_command(
140140
"""
141141
CREATE SERVER test_bad_auth TYPE 'rest'
142142
FOREIGN DATA WRAPPER iceberg_catalog
143-
OPTIONS (rest_endpoint 'http://localhost:8181', rest_auth_type 'oauth2')
143+
OPTIONS (rest_endpoint 'http://localhost:8181', rest_auth_type 'basic')
144144
""",
145145
superuser_conn,
146146
raise_error=False,
@@ -177,38 +177,23 @@ def test_reject_options_on_non_server(superuser_conn, extension):
177177
superuser_conn.rollback()
178178

179179

180-
# ── Creating foreign tables on iceberg_catalog should fail ─────────────────
180+
# ── CREATE FOREIGN TABLE on iceberg_catalog servers is blocked ──────────────
181181

182182

183-
def test_cannot_query_foreign_table_on_catalog_server(superuser_conn, extension):
184-
"""iceberg_catalog has no handler, so querying a foreign table should fail.
185-
186-
PostgreSQL allows CREATE FOREIGN TABLE on a handler-less FDW; the error
187-
only surfaces at query time when GetFdwRoutineByServerId() is called.
188-
"""
189-
run_command(
190-
"""
191-
CREATE SERVER test_ft_server TYPE 'rest'
192-
FOREIGN DATA WRAPPER iceberg_catalog
193-
OPTIONS (rest_endpoint 'http://localhost:8181')
194-
""",
195-
superuser_conn,
196-
)
197-
198-
run_command(
183+
def test_reject_create_foreign_table_on_iceberg_catalog_server(
184+
superuser_conn, extension
185+
):
186+
"""CREATE FOREIGN TABLE on an iceberg_catalog server is blocked."""
187+
err = run_command(
199188
"""
200-
CREATE FOREIGN TABLE test_ft_table (id int)
201-
SERVER test_ft_server
189+
CREATE FOREIGN TABLE test_ft_pg (id int)
190+
SERVER postgres
202191
""",
203192
superuser_conn,
204-
)
205-
206-
err = run_command(
207-
"SELECT * FROM test_ft_table",
208-
superuser_conn,
209193
raise_error=False,
210194
)
211-
assert "has no handler" in str(err)
195+
assert err is not None
196+
assert "cannot create foreign tables on iceberg_catalog server" in str(err)
212197
superuser_conn.rollback()
213198

214199

@@ -631,6 +616,50 @@ def test_reject_rename_rest_server(superuser_conn, extension):
631616
superuser_conn.rollback()
632617

633618

619+
def test_reject_owner_change_postgres_server(superuser_conn, extension):
620+
"""ALTER SERVER ... OWNER TO on the extension-owned 'postgres' server is blocked."""
621+
err = run_command(
622+
"ALTER SERVER postgres OWNER TO CURRENT_USER",
623+
superuser_conn,
624+
raise_error=False,
625+
)
626+
assert err is not None
627+
assert (
628+
'cannot change owner of the extension-owned "postgres" catalog server'
629+
in str(err)
630+
)
631+
superuser_conn.rollback()
632+
633+
634+
def test_reject_owner_change_object_store_server(superuser_conn, extension):
635+
"""ALTER SERVER ... OWNER TO on the extension-owned 'object_store' server is blocked."""
636+
err = run_command(
637+
"ALTER SERVER object_store OWNER TO CURRENT_USER",
638+
superuser_conn,
639+
raise_error=False,
640+
)
641+
assert err is not None
642+
assert (
643+
'cannot change owner of the extension-owned "object_store" catalog server'
644+
in str(err)
645+
)
646+
superuser_conn.rollback()
647+
648+
649+
def test_reject_owner_change_rest_server(superuser_conn, extension):
650+
"""ALTER SERVER ... OWNER TO on the extension-owned 'rest' server is blocked."""
651+
err = run_command(
652+
"ALTER SERVER rest OWNER TO CURRENT_USER",
653+
superuser_conn,
654+
raise_error=False,
655+
)
656+
assert err is not None
657+
assert 'cannot change owner of the extension-owned "rest" catalog server' in str(
658+
err
659+
)
660+
superuser_conn.rollback()
661+
662+
634663
def test_allow_drop_user_created_server(superuser_conn, extension):
635664
"""DROP SERVER on a user-created server should work fine."""
636665
run_command(
@@ -659,3 +688,20 @@ def test_allow_rename_user_created_server(superuser_conn, extension):
659688
"ALTER SERVER user_rename_srv RENAME TO user_renamed_srv", superuser_conn
660689
)
661690
superuser_conn.rollback()
691+
692+
693+
def test_allow_owner_change_user_created_server(superuser_conn, extension):
694+
"""ALTER SERVER ... OWNER TO on a user-created server should work fine."""
695+
run_command(
696+
"""
697+
CREATE SERVER user_owner_srv TYPE 'rest'
698+
FOREIGN DATA WRAPPER iceberg_catalog
699+
OPTIONS (rest_endpoint 'http://localhost:8181')
700+
""",
701+
superuser_conn,
702+
)
703+
run_command(
704+
"ALTER SERVER user_owner_srv OWNER TO CURRENT_USER",
705+
superuser_conn,
706+
)
707+
superuser_conn.rollback()

0 commit comments

Comments
 (0)