Skip to content

Commit ed4c0cb

Browse files
authored
Correct the handling of access delegation mode (apache#211)
1 parent fe4d16c commit ed4c0cb

File tree

11 files changed

+181
-31
lines changed

11 files changed

+181
-31
lines changed

docs/index.html

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

docs/quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ bin/spark-shell \
262262
--packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.2,org.apache.hadoop:hadoop-aws:3.4.0 \
263263
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \
264264
--conf spark.sql.catalog.quickstart_catalog.warehouse=quickstart_catalog \
265-
--conf spark.sql.catalog.quickstart_catalog.header.X-Iceberg-Access-Delegation=true \
265+
--conf spark.sql.catalog.quickstart_catalog.header.X-Iceberg-Access-Delegation=vended-credentials \
266266
--conf spark.sql.catalog.quickstart_catalog=org.apache.iceberg.spark.SparkCatalog \
267267
--conf spark.sql.catalog.quickstart_catalog.catalog-impl=org.apache.iceberg.rest.RESTCatalog \
268268
--conf spark.sql.catalog.quickstart_catalog.uri=http://localhost:8181/api/catalog \

notebooks/SparkPolaris.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@
242242
"* Catalog URI points to our Polaris installation\n",
243243
"* Credential set using the client_id and client_secret generated for the principal\n",
244244
"* Scope set to `PRINCIPAL_ROLE:ALL`\n",
245-
"* `X-Iceberg-Access-Delegation` is set to true"
245+
"* `X-Iceberg-Access-Delegation` is set to vended-credentials"
246246
]
247247
},
248248
{
@@ -278,7 +278,7 @@
278278
" .config(\"spark.sql.catalog.polaris.scope\", 'PRINCIPAL_ROLE:ALL')\n",
279279
"\n",
280280
" # Enable access credential delegation\n",
281-
" .config(\"spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation\", 'true')\n",
281+
" .config(\"spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation\", 'vended-credentials')\n",
282282
"\n",
283283
" .config(\"spark.sql.catalog.polaris.io-impl\", \"org.apache.iceberg.io.ResolvingFileIO\")\n",
284284
" .config(\"spark.sql.catalog.polaris.s3.region\", \"us-west-2\")\n",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.catalog;
20+
21+
import com.google.common.base.Functions;
22+
import java.util.Arrays;
23+
import java.util.EnumSet;
24+
import java.util.Locale;
25+
import java.util.Map;
26+
import java.util.stream.Collectors;
27+
28+
/**
29+
* Represents access mechanisms defined in the Iceberg REST API specification (values for the {@code
30+
* X-Iceberg-Access-Delegation} header).
31+
*/
32+
public enum AccessDelegationMode {
33+
UNKNOWN("unknown"),
34+
VENDED_CREDENTIALS("vended-credentials"),
35+
REMOTE_SIGNING("remote-signing"),
36+
;
37+
38+
AccessDelegationMode(String protocolValue) {
39+
this.protocolValue = protocolValue;
40+
}
41+
42+
private final String protocolValue;
43+
44+
public String protocolValue() {
45+
return protocolValue;
46+
}
47+
48+
public static EnumSet<AccessDelegationMode> fromProtocolValuesList(String protocolValues) {
49+
if (protocolValues == null || protocolValues.isEmpty()) {
50+
return EnumSet.noneOf(AccessDelegationMode.class);
51+
}
52+
53+
// Backward-compatibility case for old clients that still use the unofficial value of `true` to
54+
// request credential vending. Note that if the client requests `true` among other values it
55+
// will be parsed as `UNKNOWN` (by the code below this `if`) since the client submitting
56+
// multiple access modes is expected to be aware of the Iceberg REST API spec.
57+
if (protocolValues.trim().toLowerCase(Locale.ROOT).equals("true")) {
58+
return EnumSet.of(VENDED_CREDENTIALS);
59+
}
60+
61+
EnumSet<AccessDelegationMode> set = EnumSet.noneOf(AccessDelegationMode.class);
62+
Arrays.stream(protocolValues.split(",")) // per Iceberg REST Catalog spec
63+
.map(String::trim)
64+
.map(n -> Mapper.byProtocolValue.getOrDefault(n, UNKNOWN))
65+
.forEach(set::add);
66+
return set;
67+
}
68+
69+
private static class Mapper {
70+
private static final Map<String, AccessDelegationMode> byProtocolValue =
71+
Arrays.stream(AccessDelegationMode.values())
72+
.collect(Collectors.toMap(AccessDelegationMode::protocolValue, Functions.identity()));
73+
}
74+
}

polaris-service/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
*/
1919
package org.apache.polaris.service.catalog;
2020

21+
import static org.apache.polaris.service.catalog.AccessDelegationMode.VENDED_CREDENTIALS;
22+
2123
import com.google.common.base.Preconditions;
22-
import com.google.common.base.Strings;
2324
import com.google.common.collect.ImmutableMap;
2425
import jakarta.ws.rs.core.Response;
2526
import jakarta.ws.rs.core.SecurityContext;
2627
import java.net.URLEncoder;
2728
import java.nio.charset.Charset;
29+
import java.util.EnumSet;
2830
import java.util.List;
2931
import java.util.Map;
3032
import java.util.Optional;
@@ -171,28 +173,39 @@ public Response updateProperties(
171173
.build();
172174
}
173175

176+
private EnumSet<AccessDelegationMode> parseAccessDelegationModes(String accessDelegationMode) {
177+
EnumSet<AccessDelegationMode> delegationModes =
178+
AccessDelegationMode.fromProtocolValuesList(accessDelegationMode);
179+
Preconditions.checkArgument(
180+
delegationModes.isEmpty() || delegationModes.contains(VENDED_CREDENTIALS),
181+
"Unsupported access delegation mode: %s",
182+
accessDelegationMode);
183+
return delegationModes;
184+
}
185+
174186
@Override
175187
public Response createTable(
176188
String prefix,
177189
String namespace,
178190
CreateTableRequest createTableRequest,
179-
String xIcebergAccessDelegation,
191+
String accessDelegationMode,
180192
SecurityContext securityContext) {
193+
EnumSet<AccessDelegationMode> delegationModes =
194+
parseAccessDelegationModes(accessDelegationMode);
181195
Namespace ns = decodeNamespace(namespace);
182196
if (createTableRequest.stageCreate()) {
183-
if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) {
197+
if (delegationModes.isEmpty()) {
184198
return Response.ok(
185199
newHandlerWrapper(securityContext, prefix)
186200
.createTableStaged(ns, createTableRequest))
187201
.build();
188202
} else {
189203
return Response.ok(
190204
newHandlerWrapper(securityContext, prefix)
191-
.createTableStagedWithWriteDelegation(
192-
ns, createTableRequest, xIcebergAccessDelegation))
205+
.createTableStagedWithWriteDelegation(ns, createTableRequest))
193206
.build();
194207
}
195-
} else if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) {
208+
} else if (delegationModes.isEmpty()) {
196209
return Response.ok(
197210
newHandlerWrapper(securityContext, prefix).createTableDirect(ns, createTableRequest))
198211
.build();
@@ -220,20 +233,21 @@ public Response loadTable(
220233
String prefix,
221234
String namespace,
222235
String table,
223-
String xIcebergAccessDelegation,
236+
String accessDelegationMode,
224237
String snapshots,
225238
SecurityContext securityContext) {
239+
EnumSet<AccessDelegationMode> delegationModes =
240+
parseAccessDelegationModes(accessDelegationMode);
226241
Namespace ns = decodeNamespace(namespace);
227242
TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table));
228-
if (Strings.isNullOrEmpty(xIcebergAccessDelegation)) {
243+
if (delegationModes.isEmpty()) {
229244
return Response.ok(
230245
newHandlerWrapper(securityContext, prefix).loadTable(tableIdentifier, snapshots))
231246
.build();
232247
} else {
233248
return Response.ok(
234249
newHandlerWrapper(securityContext, prefix)
235-
.loadTableWithAccessDelegation(
236-
tableIdentifier, xIcebergAccessDelegation, snapshots))
250+
.loadTableWithAccessDelegation(tableIdentifier, snapshots))
237251
.build();
238252
}
239253
}

polaris-service/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ public LoadTableResponse createTableStaged(Namespace namespace, CreateTableReque
679679
}
680680

681681
public LoadTableResponse createTableStagedWithWriteDelegation(
682-
Namespace namespace, CreateTableRequest request, String xIcebergAccessDelegation) {
682+
Namespace namespace, CreateTableRequest request) {
683683
PolarisAuthorizableOperation op =
684684
PolarisAuthorizableOperation.CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION;
685685
authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
@@ -770,7 +770,7 @@ public LoadTableResponse loadTable(TableIdentifier tableIdentifier, String snaps
770770
}
771771

772772
public LoadTableResponse loadTableWithAccessDelegation(
773-
TableIdentifier tableIdentifier, String xIcebergAccessDelegation, String snapshots) {
773+
TableIdentifier tableIdentifier, String snapshots) {
774774
// Here we have a single method that falls through multiple candidate
775775
// PolarisAuthorizableOperations because instead of identifying the desired operation up-front
776776
// and
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.catalog;
20+
21+
import static org.apache.polaris.service.catalog.AccessDelegationMode.*;
22+
import static org.apache.polaris.service.catalog.AccessDelegationMode.REMOTE_SIGNING;
23+
import static org.apache.polaris.service.catalog.AccessDelegationMode.VENDED_CREDENTIALS;
24+
import static org.apache.polaris.service.catalog.AccessDelegationMode.fromProtocolValuesList;
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
import java.util.EnumSet;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.EnumSource;
31+
32+
class AccessDelegationModeTest {
33+
34+
@ParameterizedTest
35+
@EnumSource(AccessDelegationMode.class)
36+
void testSingle(AccessDelegationMode mode) {
37+
assertThat(fromProtocolValuesList(mode.protocolValue())).isEqualTo(EnumSet.of(mode));
38+
}
39+
40+
@Test
41+
void testSeveral() {
42+
assertThat(fromProtocolValuesList("vended-credentials, remote-signing"))
43+
.isEqualTo(EnumSet.of(VENDED_CREDENTIALS, REMOTE_SIGNING));
44+
}
45+
46+
@Test
47+
void testEmpty() {
48+
assertThat(fromProtocolValuesList(null)).isEqualTo(EnumSet.noneOf(AccessDelegationMode.class));
49+
assertThat(fromProtocolValuesList("")).isEqualTo(EnumSet.noneOf(AccessDelegationMode.class));
50+
}
51+
52+
@Test
53+
void testUnknown() {
54+
assertThat(fromProtocolValuesList("abc")).isEqualTo(EnumSet.of(UNKNOWN));
55+
assertThat(fromProtocolValuesList("abc,def")).isEqualTo(EnumSet.of(UNKNOWN));
56+
assertThat(fromProtocolValuesList("abc,remote-signing"))
57+
.isEqualTo(EnumSet.of(REMOTE_SIGNING, UNKNOWN));
58+
}
59+
60+
@Test
61+
void testLegacy() {
62+
assertThat(fromProtocolValuesList("true")).isEqualTo(EnumSet.of(VENDED_CREDENTIALS));
63+
assertThat(fromProtocolValuesList("true, vended-credentials"))
64+
.isEqualTo(EnumSet.of(UNKNOWN, VENDED_CREDENTIALS));
65+
}
66+
}

polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -706,8 +706,7 @@ public void testCreateTableStagedWithWriteDelegationAllSufficientPrivileges() {
706706
Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)),
707707
() -> {
708708
newWrapper(Set.of(PRINCIPAL_ROLE1))
709-
.createTableStagedWithWriteDelegation(
710-
NS2, createStagedWithWriteDelegationRequest, "vended-credentials");
709+
.createTableStagedWithWriteDelegation(NS2, createStagedWithWriteDelegationRequest);
711710
},
712711
// createTableStagedWithWriteDelegation doesn't actually commit any metadata
713712
null,
@@ -736,8 +735,7 @@ public void testCreateTableStagedWithWriteDelegationInsufficientPermissions() {
736735
PolarisPrivilege.TABLE_LIST),
737736
() -> {
738737
newWrapper(Set.of(PRINCIPAL_ROLE1))
739-
.createTableStagedWithWriteDelegation(
740-
NS2, createStagedWithWriteDelegationRequest, "vended-credentials");
738+
.createTableStagedWithWriteDelegation(NS2, createStagedWithWriteDelegationRequest);
741739
});
742740
}
743741

@@ -855,7 +853,7 @@ public void testLoadTableWithReadAccessDelegationSufficientPrivileges() {
855853
PolarisPrivilege.TABLE_READ_DATA,
856854
PolarisPrivilege.TABLE_WRITE_DATA,
857855
PolarisPrivilege.CATALOG_MANAGE_CONTENT),
858-
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"),
856+
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "all"),
859857
null /* cleanupAction */);
860858
}
861859

@@ -871,8 +869,7 @@ public void testLoadTableWithReadAccessDelegationInsufficientPermissions() {
871869
PolarisPrivilege.TABLE_CREATE,
872870
PolarisPrivilege.TABLE_LIST,
873871
PolarisPrivilege.TABLE_DROP),
874-
() ->
875-
newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"));
872+
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "all"));
876873
}
877874

878875
@Test
@@ -885,7 +882,7 @@ public void testLoadTableWithWriteAccessDelegationSufficientPrivileges() {
885882
PolarisPrivilege.TABLE_READ_DATA,
886883
PolarisPrivilege.TABLE_WRITE_DATA,
887884
PolarisPrivilege.CATALOG_MANAGE_CONTENT),
888-
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"),
885+
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "all"),
889886
null /* cleanupAction */);
890887
}
891888

@@ -901,8 +898,7 @@ public void testLoadTableWithWriteAccessDelegationInsufficientPermissions() {
901898
PolarisPrivilege.TABLE_CREATE,
902899
PolarisPrivilege.TABLE_LIST,
903900
PolarisPrivilege.TABLE_DROP),
904-
() ->
905-
newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "vended-credentials", "all"));
901+
() -> newWrapper().loadTableWithAccessDelegation(TABLE_NS1A_2, "all"));
906902
}
907903

908904
@Test

regtests/setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ spark.driver.extraJavaOptions -Dderby.system.home=${DERBY_HOME}
112112
spark.sql.catalog.polaris=org.apache.iceberg.spark.SparkCatalog
113113
spark.sql.catalog.polaris.type=rest
114114
spark.sql.catalog.polaris.uri=http://${POLARIS_HOST:-localhost}:8181/api/catalog
115-
spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation=true
115+
spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation=vended-credentials
116116
spark.sql.catalog.polaris.client.region=us-west-2
117117
EOF
118118
echo 'Success!'

regtests/t_pyspark/src/iceberg_spark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __enter__(self):
102102
.config(
103103
f"spark.sql.catalog.{catalog_name}", "org.apache.iceberg.spark.SparkCatalog"
104104
)
105-
.config(f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation", "true")
105+
.config(f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation", "vended-credentials")
106106
.config(f"spark.sql.catalog.{catalog_name}.type", "rest")
107107
.config(f"spark.sql.catalog.{catalog_name}.uri", self.polaris_url)
108108
.config(f"spark.sql.catalog.{catalog_name}.warehouse", self.catalog_name)

0 commit comments

Comments
 (0)