1919package org .apache .polaris .service .it ;
2020
2121import static java .nio .charset .StandardCharsets .UTF_8 ;
22+ import static org .apache .iceberg .aws .AwsClientProperties .REFRESH_CREDENTIALS_ENDPOINT ;
23+ import static org .apache .iceberg .aws .s3 .S3FileIOProperties .ACCESS_KEY_ID ;
24+ import static org .apache .iceberg .aws .s3 .S3FileIOProperties .ENDPOINT ;
25+ import static org .apache .iceberg .aws .s3 .S3FileIOProperties .SECRET_ACCESS_KEY ;
2226import static org .apache .iceberg .types .Types .NestedField .optional ;
2327import static org .apache .iceberg .types .Types .NestedField .required ;
28+ import static org .apache .polaris .service .catalog .AccessDelegationMode .VENDED_CREDENTIALS ;
2429import static org .apache .polaris .service .it .env .PolarisClient .polarisClient ;
2530import static org .assertj .core .api .Assertions .assertThat ;
2631
4247import org .apache .iceberg .Schema ;
4348import org .apache .iceberg .Table ;
4449import org .apache .iceberg .TableOperations ;
45- import org .apache .iceberg .aws .AwsClientProperties ;
4650import org .apache .iceberg .catalog .TableIdentifier ;
4751import org .apache .iceberg .io .FileIO ;
4852import org .apache .iceberg .io .OutputFile ;
5761import org .apache .polaris .core .admin .model .PolarisCatalog ;
5862import org .apache .polaris .core .admin .model .PrincipalWithCredentials ;
5963import org .apache .polaris .core .admin .model .StorageConfigInfo ;
64+ import org .apache .polaris .service .catalog .AccessDelegationMode ;
6065import org .apache .polaris .service .it .env .CatalogApi ;
6166import org .apache .polaris .service .it .env .ClientCredentials ;
6267import org .apache .polaris .service .it .env .ManagementApi ;
7479import org .junit .jupiter .api .TestInfo ;
7580import org .junit .jupiter .api .extension .ExtendWith ;
7681import org .junit .jupiter .params .ParameterizedTest ;
82+ import org .junit .jupiter .params .provider .CsvSource ;
7783import org .junit .jupiter .params .provider .ValueSource ;
7884import software .amazon .awssdk .services .s3 .S3Client ;
7985import software .amazon .awssdk .services .s3 .model .GetObjectRequest ;
@@ -112,7 +118,6 @@ public Map<String, String> getConfigOverrides() {
112118 required (1 , "id" , Types .IntegerType .get (), "doc" ),
113119 optional (2 , "data" , Types .StringType .get ()));
114120
115- private static ClientCredentials adminCredentials ;
116121 private static PolarisApiEndpoints endpoints ;
117122 private static PolarisClient client ;
118123 private static ManagementApi managementApi ;
@@ -131,7 +136,6 @@ static void setup(
131136 @ Minio (accessKey = MINIO_ACCESS_KEY , secretKey = MINIO_SECRET_KEY ) MinioAccess minioAccess ,
132137 ClientCredentials credentials ) {
133138 s3Client = minioAccess .s3Client ();
134- adminCredentials = credentials ;
135139 endpoints = apiEndpoints ;
136140 client = polarisClient (endpoints );
137141 adminToken = client .obtainToken (credentials );
@@ -158,15 +162,19 @@ public void before(TestInfo testInfo) {
158162 }
159163
160164 private RESTCatalog createCatalog (
161- Optional <String > endpoint , Optional <String > stsEndpoint , boolean pathStyleAccess ) {
162- return createCatalog (endpoint , stsEndpoint , pathStyleAccess , Optional .empty ());
165+ Optional <String > endpoint ,
166+ Optional <String > stsEndpoint ,
167+ boolean pathStyleAccess ,
168+ Optional <AccessDelegationMode > delegationMode ) {
169+ return createCatalog (endpoint , stsEndpoint , pathStyleAccess , Optional .empty (), delegationMode );
163170 }
164171
165172 private RESTCatalog createCatalog (
166173 Optional <String > endpoint ,
167174 Optional <String > stsEndpoint ,
168175 boolean pathStyleAccess ,
169- Optional <String > endpointInternal ) {
176+ Optional <String > endpointInternal ,
177+ Optional <AccessDelegationMode > delegationMode ) {
170178 AwsStorageConfigInfo .Builder storageConfig =
171179 AwsStorageConfigInfo .builder ()
172180 .setStorageType (StorageConfigInfo .StorageTypeEnum .S3 )
@@ -198,8 +206,16 @@ private RESTCatalog createCatalog(
198206 org .apache .iceberg .CatalogProperties .URI , endpoints .catalogApiEndpoint ().toString ())
199207 .put (OAuth2Properties .TOKEN , authToken )
200208 .put ("warehouse" , catalogName )
201- .putAll (endpoints .extraHeaders ("header." ))
202- .put ("header.X-Iceberg-Access-Delegation" , "vended-credentials" );
209+ .putAll (endpoints .extraHeaders ("header." ));
210+
211+ delegationMode .ifPresent (
212+ dm -> propertiesBuilder .put ("header.X-Iceberg-Access-Delegation" , dm .protocolValue ()));
213+
214+ if (delegationMode .isEmpty ()) {
215+ // Use local credentials on the client side
216+ propertiesBuilder .put ("s3.access-key-id" , MINIO_ACCESS_KEY );
217+ propertiesBuilder .put ("s3.secret-access-key" , MINIO_SECRET_KEY );
218+ }
203219
204220 restCatalog .initialize ("polaris" , propertiesBuilder .buildKeepingLast ());
205221 return restCatalog ;
@@ -213,13 +229,34 @@ public void cleanUp() {
213229 @ ParameterizedTest
214230 @ ValueSource (booleans = {true , false })
215231 public void testCreateTable (boolean pathStyle ) throws IOException {
232+ LoadTableResponse response = doTestCreateTable (pathStyle , Optional .empty ());
233+ assertThat (response .config ()).doesNotContainKey (SECRET_ACCESS_KEY );
234+ assertThat (response .config ()).doesNotContainKey (ACCESS_KEY_ID );
235+ assertThat (response .config ()).doesNotContainKey (REFRESH_CREDENTIALS_ENDPOINT );
236+ assertThat (response .credentials ()).isEmpty ();
237+ }
238+
239+ @ ParameterizedTest
240+ @ ValueSource (booleans = {true , false })
241+ public void testCreateTableVendedCredentials (boolean pathStyle ) throws IOException {
242+ LoadTableResponse response = doTestCreateTable (pathStyle , Optional .of (VENDED_CREDENTIALS ));
243+ assertThat (response .config ())
244+ .containsEntry (
245+ REFRESH_CREDENTIALS_ENDPOINT ,
246+ "v1/" + catalogName + "/namespaces/test-ns/tables/t1/credentials" );
247+ assertThat (response .credentials ()).hasSize (1 );
248+ }
249+
250+ private LoadTableResponse doTestCreateTable (boolean pathStyle , Optional <AccessDelegationMode > dm )
251+ throws IOException {
216252 try (RESTCatalog restCatalog =
217- createCatalog (Optional .of (endpoint ), Optional .empty (), pathStyle )) {
218- LoadTableResponse loadTableResponse = doTestCreateTable (restCatalog );
253+ createCatalog (Optional .of (endpoint ), Optional .empty (), pathStyle , dm )) {
254+ LoadTableResponse loadTableResponse = doTestCreateTable (restCatalog , dm );
219255 if (pathStyle ) {
220256 assertThat (loadTableResponse .config ())
221257 .containsEntry ("s3.path-style-access" , Boolean .TRUE .toString ());
222258 }
259+ return loadTableResponse ;
223260 }
224261 }
225262
@@ -230,7 +267,8 @@ public void testInternalEndpoints() throws IOException {
230267 Optional .of ("http://s3.example.com" ),
231268 Optional .of (endpoint ),
232269 false ,
233- Optional .of (endpoint ))) {
270+ Optional .of (endpoint ),
271+ Optional .empty ())) {
234272 StorageConfigInfo storageConfig =
235273 managementApi .getCatalog (catalogName ).getStorageConfigInfo ();
236274 assertThat ((AwsStorageConfigInfo ) storageConfig )
@@ -240,12 +278,13 @@ public void testInternalEndpoints() throws IOException {
240278 AwsStorageConfigInfo ::getEndpointInternal ,
241279 AwsStorageConfigInfo ::getPathStyleAccess )
242280 .containsExactly ("http://s3.example.com" , endpoint , endpoint , false );
243- LoadTableResponse loadTableResponse = doTestCreateTable (restCatalog );
244- assertThat (loadTableResponse .config ()).containsEntry ("s3.endpoint" , "http://s3.example.com" );
281+ LoadTableResponse loadTableResponse = doTestCreateTable (restCatalog , Optional . empty () );
282+ assertThat (loadTableResponse .config ()).containsEntry (ENDPOINT , "http://s3.example.com" );
245283 }
246284 }
247285
248- public LoadTableResponse doTestCreateTable (RESTCatalog restCatalog ) throws IOException {
286+ public LoadTableResponse doTestCreateTable (
287+ RESTCatalog restCatalog , Optional <AccessDelegationMode > dm ) {
249288 catalogApi .createNamespace (catalogName , "test-ns" );
250289 TableIdentifier id = TableIdentifier .of ("test-ns" , "t1" );
251290 Table table = restCatalog .createTable (id , SCHEMA );
@@ -266,23 +305,32 @@ public LoadTableResponse doTestCreateTable(RESTCatalog restCatalog) throws IOExc
266305 assertThat (response .contentLength ()).isGreaterThan (0 );
267306
268307 LoadTableResponse loadTableResponse =
269- catalogApi .loadTableWithAccessDelegation (catalogName , id , "ALL" );
270- assertThat (loadTableResponse .config ())
271- .containsKey ("s3.endpoint" )
272- .containsEntry (
273- AwsClientProperties .REFRESH_CREDENTIALS_ENDPOINT ,
274- "v1/" + catalogName + "/namespaces/test-ns/tables/t1/credentials" );
308+ catalogApi .loadTable (
309+ catalogName ,
310+ id ,
311+ "ALL" ,
312+ dm .map (v -> Map .of ("X-Iceberg-Access-Delegation" , v .protocolValue ())).orElse (Map .of ()));
313+
314+ assertThat (loadTableResponse .config ()).containsKey (ENDPOINT );
275315
276316 restCatalog .dropTable (id );
277317 assertThat (restCatalog .tableExists (id )).isFalse ();
278318 return loadTableResponse ;
279319 }
280320
281321 @ ParameterizedTest
282- @ ValueSource (booleans = {true , false })
283- public void testAppendFiles (boolean pathStyle ) throws IOException {
322+ @ CsvSource ("true," )
323+ @ CsvSource ("false," )
324+ @ CsvSource ("true,VENDED_CREDENTIALS" )
325+ @ CsvSource ("false,VENDED_CREDENTIALS" )
326+ public void testAppendFiles (boolean pathStyle , AccessDelegationMode delegationMode )
327+ throws IOException {
284328 try (RESTCatalog restCatalog =
285- createCatalog (Optional .of (endpoint ), Optional .of (endpoint ), pathStyle )) {
329+ createCatalog (
330+ Optional .of (endpoint ),
331+ Optional .of (endpoint ),
332+ pathStyle ,
333+ Optional .ofNullable (delegationMode ))) {
286334 catalogApi .createNamespace (catalogName , "test-ns" );
287335 TableIdentifier id = TableIdentifier .of ("test-ns" , "t1" );
288336 Table table = restCatalog .createTable (id , SCHEMA );
@@ -295,7 +343,8 @@ public void testAppendFiles(boolean pathStyle) throws IOException {
295343 URI .create (
296344 table
297345 .locationProvider ()
298- .newDataLocation (String .format ("test-file-%s.txt" , pathStyle )));
346+ .newDataLocation (
347+ String .format ("test-file-%s-%s.txt" , pathStyle , delegationMode )));
299348 OutputFile f1 = io .newOutputFile (loc .toString ());
300349 try (PositionOutputStream os = f1 .create ()) {
301350 os .write ("Hello World" .getBytes (UTF_8 ));
0 commit comments