2727import java .util .UUID ;
2828import org .apache .iceberg .exceptions .ForbiddenException ;
2929import org .apache .polaris .core .admin .model .AuthenticationParameters ;
30+ import org .apache .polaris .core .admin .model .AwsStorageConfigInfo ;
3031import org .apache .polaris .core .admin .model .Catalog ;
3132import org .apache .polaris .core .admin .model .CatalogGrant ;
3233import org .apache .polaris .core .admin .model .CatalogPrivilege ;
3334import org .apache .polaris .core .admin .model .CatalogProperties ;
3435import org .apache .polaris .core .admin .model .CatalogRole ;
3536import org .apache .polaris .core .admin .model .ConnectionConfigInfo ;
3637import org .apache .polaris .core .admin .model .ExternalCatalog ;
37- import org .apache .polaris .core .admin .model .FileStorageConfigInfo ;
3838import org .apache .polaris .core .admin .model .GrantResource ;
3939import org .apache .polaris .core .admin .model .IcebergRestConnectionConfigInfo ;
4040import org .apache .polaris .core .admin .model .NamespaceGrant ;
5252import org .apache .polaris .service .it .env .PolarisClient ;
5353import org .apache .polaris .service .it .ext .PolarisIntegrationTestExtension ;
5454import org .apache .polaris .service .it .ext .SparkSessionBuilder ;
55+ import org .apache .polaris .test .minio .Minio ;
56+ import org .apache .polaris .test .minio .MinioAccess ;
57+ import org .apache .polaris .test .minio .MinioExtension ;
5558import org .apache .spark .sql .Row ;
5659import org .apache .spark .sql .SparkSession ;
5760import org .junit .jupiter .api .AfterAll ;
6669 * Integration test for catalog federation functionality. This test verifies that an external
6770 * catalog can be created that federates with an internal catalog.
6871 */
72+ @ ExtendWith (MinioExtension .class )
6973@ ExtendWith (PolarisIntegrationTestExtension .class )
7074public class CatalogFederationIntegrationTest {
7175
76+ public static final String BUCKET_URI_PREFIX = "/minio-test-catalog-federation" ;
77+ public static final String MINIO_ACCESS_KEY = "test-ak-123-catalog-federation" ;
78+ public static final String MINIO_SECRET_KEY = "test-sk-123-catalog-federation" ;
79+
7280 private static PolarisClient client ;
7381 private static CatalogApi catalogApi ;
7482 private static ManagementApi managementApi ;
@@ -78,6 +86,8 @@ public class CatalogFederationIntegrationTest {
7886 private static String federatedCatalogName ;
7987 private static String localCatalogRoleName ;
8088 private static String federatedCatalogRoleName ;
89+ private static URI storageBase ;
90+ private static String endpoint ;
8191
8292 private static final String PRINCIPAL_NAME = "test-catalog-federation-user" ;
8393 private static final String PRINCIPAL_ROLE_NAME = "test-catalog-federation-user-role" ;
@@ -93,12 +103,17 @@ public class CatalogFederationIntegrationTest {
93103 private PrincipalWithCredentials newUserCredentials ;
94104
95105 @ BeforeAll
96- static void setup (PolarisApiEndpoints apiEndpoints , ClientCredentials credentials ) {
106+ static void setup (
107+ PolarisApiEndpoints apiEndpoints ,
108+ ClientCredentials credentials ,
109+ @ Minio (accessKey = MINIO_ACCESS_KEY , secretKey = MINIO_SECRET_KEY ) MinioAccess minioAccess ) {
97110 endpoints = apiEndpoints ;
98111 client = polarisClient (endpoints );
99112 String adminToken = client .obtainToken (credentials );
100113 managementApi = client .managementApi (adminToken );
101114 catalogApi = client .catalogApi (adminToken );
115+ storageBase = minioAccess .s3BucketUri (BUCKET_URI_PREFIX );
116+ endpoint = minioAccess .s3endpoint ();
102117 }
103118
104119 @ AfterAll
@@ -129,12 +144,14 @@ void after() {
129144 }
130145
131146 private void setupCatalogs () {
132- baseLocation = URI . create ( "file:///tmp/warehouse" ) ;
147+ baseLocation = storageBase ;
133148 newUserCredentials = managementApi .createPrincipalWithRole (PRINCIPAL_NAME , PRINCIPAL_ROLE_NAME );
134149
135- FileStorageConfigInfo storageConfig =
136- FileStorageConfigInfo .builder ()
137- .setStorageType (StorageConfigInfo .StorageTypeEnum .FILE )
150+ AwsStorageConfigInfo storageConfig =
151+ AwsStorageConfigInfo .builder ()
152+ .setStorageType (StorageConfigInfo .StorageTypeEnum .S3 )
153+ .setPathStyleAccess (true )
154+ .setEndpoint (endpoint )
138155 .setAllowedLocations (List .of (baseLocation .toString ()))
139156 .build ();
140157
@@ -197,6 +214,14 @@ private void setupCatalogs() {
197214 spark =
198215 SparkSessionBuilder .buildWithTestDefaults ()
199216 .withWarehouse (warehouseDir .toUri ())
217+ .withConfig (
218+ "spark.sql.catalog." + localCatalogName + ".header.X-Iceberg-Access-Delegation" ,
219+ "vended-credentials" )
220+ .withConfig (
221+ "spark.sql.catalog." + federatedCatalogName + ".header.X-Iceberg-Access-Delegation" ,
222+ "vended-credentials" )
223+ .withConfig ("spark.sql.catalog." + localCatalogName + ".cache-enabled" , "false" )
224+ .withConfig ("spark.sql.catalog." + federatedCatalogName + ".cache-enabled" , "false" )
200225 .addCatalog (
201226 localCatalogName , "org.apache.iceberg.spark.SparkCatalog" , endpoints , sparkToken )
202227 .addCatalog (
@@ -296,10 +321,6 @@ void testFederatedCatalogWithNamespaceRBAC() {
296321 .sql ("SELECT * FROM " + localCatalogName + ".ns2.test_table ORDER BY id" )
297322 .collectAsList ();
298323 assertThat (localNs2Data ).hasSize (2 );
299-
300- // Restore the grant
301- managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , namespaceGrant );
302- managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , defaultCatalogGrant );
303324 }
304325
305326 @ Test
@@ -335,9 +356,76 @@ void testFederatedCatalogWithTableRBAC() {
335356 .sql ("SELECT * FROM " + localCatalogName + ".ns2.test_table ORDER BY id" )
336357 .collectAsList ();
337358 assertThat (localNs2Data ).hasSize (2 );
359+ }
338360
339- // Restore the grant
340- managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , tableGrant );
341- managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , defaultCatalogGrant );
361+ @ Test
362+ void testFederatedCatalogWithCredentialVending () {
363+ managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , defaultCatalogGrant );
364+
365+ // Case 1: Only have TABLE_READ_PROPERTIES privilege, should not be able to read data
366+ TableGrant tablePropertiesGrant =
367+ TableGrant .builder ()
368+ .setType (GrantResource .TypeEnum .TABLE )
369+ .setPrivilege (TablePrivilege .TABLE_READ_PROPERTIES )
370+ .setNamespace (List .of ("ns1" ))
371+ .setTableName ("test_table" )
372+ .build ();
373+ managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , tablePropertiesGrant );
374+ spark .sql ("USE " + federatedCatalogName );
375+
376+ // Read table data should fail since TABLE_READ_PROPERTIES does not allow reading data
377+ assertThatThrownBy (() -> spark .sql ("SELECT * FROM ns1.test_table ORDER BY id" ))
378+ .isInstanceOf (ForbiddenException .class );
379+
380+ // Case 2: Only have TABLE_READ_DATA privilege, should be able to read data but not write
381+ managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , tablePropertiesGrant );
382+ TableGrant tableReadDataGrant =
383+ TableGrant .builder ()
384+ .setType (GrantResource .TypeEnum .TABLE )
385+ .setPrivilege (TablePrivilege .TABLE_READ_DATA )
386+ .setNamespace (List .of ("ns1" ))
387+ .setTableName ("test_table" )
388+ .build ();
389+ managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , tableReadDataGrant );
390+
391+ // Verify that the vended credential allows reading the data
392+ List <Row > ns1Data = spark .sql ("SELECT * FROM ns1.test_table ORDER BY id" ).collectAsList ();
393+ assertThat (ns1Data ).hasSize (2 );
394+ assertThat (ns1Data .get (0 ).getInt (0 )).isEqualTo (1 );
395+ assertThat (ns1Data .get (0 ).getString (1 )).isEqualTo ("Alice" );
396+
397+ // Verify that write is blocked since the vended credential should only have read permission
398+ assertThatThrownBy (() -> spark .sql ("INSERT INTO ns1.test_table VALUES (3, 'Charlie')" ))
399+ .hasMessageContaining (
400+ "software.amazon.awssdk.services.s3.model.S3Exception: Access Denied. (Service: S3, Status Code: 403," );
401+
402+ // Case 3: TABLE_WRITE_DATA should
403+ managementApi .revokeGrant (federatedCatalogName , federatedCatalogRoleName , tableReadDataGrant );
404+ TableGrant tableWriteDataGrant =
405+ TableGrant .builder ()
406+ .setType (GrantResource .TypeEnum .TABLE )
407+ .setPrivilege (TablePrivilege .TABLE_WRITE_DATA )
408+ .setNamespace (List .of ("ns1" ))
409+ .setTableName ("test_table" )
410+ .build ();
411+ managementApi .addGrant (federatedCatalogName , federatedCatalogRoleName , tableWriteDataGrant );
412+
413+ spark .sql ("INSERT INTO ns1.test_table VALUES (3, 'Charlie')" );
414+
415+ // Verify the write was successful by reading back
416+ List <Row > updatedData = spark .sql ("SELECT * FROM ns1.test_table ORDER BY id" ).collectAsList ();
417+ assertThat (updatedData ).hasSize (3 );
418+ assertThat (updatedData .get (2 ).getInt (0 )).isEqualTo (3 );
419+ assertThat (updatedData .get (2 ).getString (1 )).isEqualTo ("Charlie" );
420+
421+ // Verify the data is also visible from the local catalog (both point to same storage)
422+ spark .sql (String .format ("REFRESH TABLE %s.ns1.test_table" , localCatalogName ));
423+ List <Row > localData =
424+ spark
425+ .sql (String .format ("SELECT * FROM %s.ns1.test_table ORDER BY id" , localCatalogName ))
426+ .collectAsList ();
427+ assertThat (localData ).hasSize (3 );
428+ assertThat (localData .get (2 ).getInt (0 )).isEqualTo (3 );
429+ assertThat (localData .get (2 ).getString (1 )).isEqualTo ("Charlie" );
342430 }
343431}
0 commit comments