@@ -86,7 +86,10 @@ public class CatalogFederationIntegrationTest {
8686 private static String federatedCatalogName ;
8787 private static String localCatalogRoleName ;
8888 private static String federatedCatalogRoleName ;
89- private static URI storageBase ;
89+ private static URI localStorageBase ;
90+ private static URI remoteStorageBase ;
91+ private static URI remoteStorageExtraAllowedLocationNs1 ;
92+ private static URI remoteStorageExtraAllowedLocationNs2 ;
9093 private static String endpoint ;
9194
9295 private static final String PRINCIPAL_NAME = "test-catalog-federation-user" ;
@@ -99,7 +102,6 @@ public class CatalogFederationIntegrationTest {
99102
100103 @ TempDir static java .nio .file .Path warehouseDir ;
101104
102- private URI baseLocation ;
103105 private PrincipalWithCredentials newUserCredentials ;
104106
105107 @ BeforeAll
@@ -112,8 +114,15 @@ static void setup(
112114 String adminToken = client .obtainToken (credentials );
113115 managementApi = client .managementApi (adminToken );
114116 catalogApi = client .catalogApi (adminToken );
115- storageBase = minioAccess .s3BucketUri (BUCKET_URI_PREFIX );
116117 endpoint = minioAccess .s3endpoint ();
118+
119+ localStorageBase = minioAccess .s3BucketUri (BUCKET_URI_PREFIX + "/local_catalog" );
120+ remoteStorageBase = minioAccess .s3BucketUri (BUCKET_URI_PREFIX + "/federated_catalog" );
121+ // Allow credential vending for tables located under ns1
122+ remoteStorageExtraAllowedLocationNs1 =
123+ minioAccess .s3BucketUri (BUCKET_URI_PREFIX + "/local_catalog/ns1" );
124+ remoteStorageExtraAllowedLocationNs2 =
125+ minioAccess .s3BucketUri (BUCKET_URI_PREFIX + "/local_catalog/ns2" );
117126 }
118127
119128 @ AfterAll
@@ -144,18 +153,17 @@ void after() {
144153 }
145154
146155 private void setupCatalogs () {
147- baseLocation = storageBase ;
148156 newUserCredentials = managementApi .createPrincipalWithRole (PRINCIPAL_NAME , PRINCIPAL_ROLE_NAME );
149157
150158 AwsStorageConfigInfo storageConfig =
151159 AwsStorageConfigInfo .builder ()
152160 .setStorageType (StorageConfigInfo .StorageTypeEnum .S3 )
153161 .setPathStyleAccess (true )
154162 .setEndpoint (endpoint )
155- .setAllowedLocations (List .of (baseLocation .toString ()))
163+ .setAllowedLocations (List .of (localStorageBase .toString ()))
156164 .build ();
157165
158- CatalogProperties catalogProperties = new CatalogProperties (baseLocation .toString ());
166+ CatalogProperties catalogProperties = new CatalogProperties (localStorageBase .toString ());
159167
160168 localCatalogName = "test_catalog_local_" + UUID .randomUUID ().toString ().replace ("-" , "" );
161169 localCatalogRoleName = "test-catalog-role_" + UUID .randomUUID ().toString ().replace ("-" , "" );
@@ -193,13 +201,26 @@ private void setupCatalogs() {
193201 .setRemoteCatalogName (localCatalogName )
194202 .setAuthenticationParameters (authParams )
195203 .build ();
204+ CatalogProperties externalCatalogProperties =
205+ new CatalogProperties (remoteStorageBase .toString ());
206+ AwsStorageConfigInfo externalStorageConfig =
207+ AwsStorageConfigInfo .builder ()
208+ .setStorageType (StorageConfigInfo .StorageTypeEnum .S3 )
209+ .setPathStyleAccess (true )
210+ .setEndpoint (endpoint )
211+ .setAllowedLocations (
212+ List .of (
213+ remoteStorageBase .toString (),
214+ remoteStorageExtraAllowedLocationNs1 .toString (),
215+ remoteStorageExtraAllowedLocationNs2 .toString ()))
216+ .build ();
196217 ExternalCatalog externalCatalog =
197218 ExternalCatalog .builder ()
198219 .setType (Catalog .TypeEnum .EXTERNAL )
199220 .setName (federatedCatalogName )
200221 .setConnectionConfigInfo (connectionConfig )
201- .setProperties (catalogProperties )
202- .setStorageConfigInfo (storageConfig )
222+ .setProperties (externalCatalogProperties )
223+ .setStorageConfigInfo (externalStorageConfig )
203224 .build ();
204225 managementApi .createCatalog (externalCatalog );
205226 managementApi .createCatalogRole (federatedCatalogName , federatedCatalogRoleName );
@@ -244,6 +265,11 @@ private void setupExampleNamespacesAndTables() {
244265 spark .sql ("INSERT INTO ns2.test_table VALUES (1, 'Apache Spark')" );
245266 spark .sql ("INSERT INTO ns2.test_table VALUES (2, 'Apache Iceberg')" );
246267
268+ spark .sql ("CREATE NAMESPACE IF NOT EXISTS ns3" );
269+ spark .sql ("CREATE TABLE IF NOT EXISTS ns3.test_table (id int, name string)" );
270+ spark .sql ("INSERT INTO ns3.test_table VALUES (1, 'Apache Spark')" );
271+ spark .sql ("INSERT INTO ns3.test_table VALUES (2, 'Apache Iceberg')" );
272+
247273 spark .sql ("CREATE NAMESPACE IF NOT EXISTS ns1.ns1a" );
248274 spark .sql ("CREATE TABLE IF NOT EXISTS ns1.ns1a.test_table (id int, name string)" );
249275 spark .sql ("INSERT INTO ns1.ns1a.test_table VALUES (1, 'Alice')" );
@@ -256,7 +282,7 @@ private void setupExampleNamespacesAndTables() {
256282 void testFederatedCatalogBasicReadWriteOperations () {
257283 spark .sql ("USE " + federatedCatalogName );
258284 List <Row > namespaces = spark .sql ("SHOW NAMESPACES" ).collectAsList ();
259- assertThat (namespaces ).hasSize (2 );
285+ assertThat (namespaces ).hasSize (3 );
260286 List <Row > ns1Data = spark .sql ("SELECT * FROM ns1.test_table ORDER BY id" ).collectAsList ();
261287 List <Row > refNs1Data =
262288 spark
@@ -428,4 +454,45 @@ void testFederatedCatalogWithCredentialVending() {
428454 assertThat (localData .get (2 ).getInt (0 )).isEqualTo (3 );
429455 assertThat (localData .get (2 ).getString (1 )).isEqualTo ("Charlie" );
430456 }
457+
458+ @ Test
459+ void testFederatedCatalogNotVendCredentialForTablesOutsideAllowedLocations () {
460+ managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , defaultCatalogGrant );
461+
462+ spark .sql ("USE " + federatedCatalogName );
463+
464+ // Case 1: Only have TABLE_READ_DATA privilege
465+ TableGrant tableReadDataGrant =
466+ TableGrant .builder ()
467+ .setType (GrantResource .TypeEnum .TABLE )
468+ .setPrivilege (TablePrivilege .TABLE_READ_DATA )
469+ .setNamespace (List .of ("ns3" ))
470+ .setTableName ("test_table" )
471+ .build ();
472+ managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , tableReadDataGrant );
473+
474+ // Verify that credential vending is blocked for table under ns3, even with enough privilege
475+ assertThatThrownBy (() -> spark .sql ("SELECT * FROM ns3.test_table ORDER BY id" ).collectAsList ())
476+ .isInstanceOf (ForbiddenException .class )
477+ .hasMessageContaining (
478+ "Table 'ns3.test_table' in remote catalog has locations outside catalog's allowed locations:" );
479+
480+ // Case 3: TABLE_WRITE_DATA
481+ managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , tableReadDataGrant );
482+ TableGrant tableWriteDataGrant =
483+ TableGrant .builder ()
484+ .setType (GrantResource .TypeEnum .TABLE )
485+ .setPrivilege (TablePrivilege .TABLE_WRITE_DATA )
486+ .setNamespace (List .of ("ns3" ))
487+ .setTableName ("test_table" )
488+ .build ();
489+ managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , tableWriteDataGrant );
490+
491+ // Verify that credential vending is blocked for table under ns3, even with enough privilege
492+ assertThatThrownBy (
493+ () -> spark .sql ("INSERT INTO ns3.test_table VALUES (3, 'Charlie')" ).collectAsList ())
494+ .isInstanceOf (ForbiddenException .class )
495+ .hasMessageContaining (
496+ "Table 'ns3.test_table' in remote catalog has locations outside catalog's allowed locations:" );
497+ }
431498}
0 commit comments