Skip to content

Commit 48aa8ef

Browse files
committed
Add support for S3 request signing
1 parent 7fbd3ab commit 48aa8ef

File tree

21 files changed

+1066
-47
lines changed

21 files changed

+1066
-47
lines changed

LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ License: https://www.apache.org/licenses/LICENSE-2.0
216216
This product includes code from Apache Iceberg.
217217

218218
* spec/iceberg-rest-catalog-open-api.yaml
219+
* spec/iceberg-s3-signer-open-api.yaml
219220
* spec/polaris-catalog-apis/oauth-tokens-api.yaml
220221
* integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
221222
* runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java

api/iceberg-service/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
implementation(platform(libs.iceberg.bom))
3232
implementation("org.apache.iceberg:iceberg-api")
3333
implementation("org.apache.iceberg:iceberg-core")
34+
implementation("org.apache.iceberg:iceberg-aws")
3435

3536
implementation(libs.jakarta.annotation.api)
3637
implementation(libs.jakarta.inject.api)
@@ -49,6 +50,9 @@ dependencies {
4950
implementation("com.fasterxml.jackson.core:jackson-databind")
5051

5152
compileOnly(libs.microprofile.fault.tolerance.api)
53+
54+
compileOnly(project(":polaris-immutables"))
55+
annotationProcessor(project(":polaris-immutables", configuration = "processor"))
5256
}
5357

5458
val rootDir = rootProject.layout.projectDirectory
@@ -112,6 +116,10 @@ openApiGenerate {
112116
"IcebergErrorResponse" to "org.apache.iceberg.rest.responses.ErrorResponse",
113117
"OAuthError" to "org.apache.iceberg.rest.responses.ErrorResponse",
114118

119+
// S3 Signing
120+
"S3SignRequest" to "org.apache.polaris.service.types.S3SignRequest",
121+
"SignS3Request200Response" to "org.apache.polaris.service.types.S3SignResponse",
122+
115123
// Custom types defined below
116124
"CommitViewRequest" to "org.apache.polaris.service.types.CommitViewRequest",
117125
"TokenType" to "org.apache.polaris.service.types.TokenType",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
20+
package org.apache.polaris.service.types;
21+
22+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
23+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
24+
import jakarta.annotation.Nullable;
25+
import java.net.URI;
26+
import java.util.List;
27+
import java.util.Map;
28+
import org.apache.iceberg.rest.RESTRequest;
29+
import org.apache.polaris.immutables.PolarisImmutable;
30+
import org.immutables.value.Value;
31+
32+
/**
33+
* Request for S3 signing requests.
34+
*
35+
* <p>Copy of {@link org.apache.iceberg.aws.s3.signer.S3SignRequest}, because the original does not
36+
* have Jackson annotations.
37+
*/
38+
@PolarisImmutable
39+
@JsonDeserialize(as = ImmutableS3SignRequest.class)
40+
@JsonSerialize(as = ImmutableS3SignRequest.class)
41+
public interface S3SignRequest extends RESTRequest {
42+
43+
String region();
44+
45+
String method();
46+
47+
URI uri();
48+
49+
Map<String, List<String>> headers();
50+
51+
Map<String, String> properties();
52+
53+
@Value.Default
54+
@Nullable
55+
default String body() {
56+
return null;
57+
}
58+
59+
@Override
60+
default void validate() {}
61+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
20+
package org.apache.polaris.service.types;
21+
22+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
23+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
24+
import java.net.URI;
25+
import java.util.List;
26+
import java.util.Map;
27+
import org.apache.iceberg.rest.RESTResponse;
28+
import org.apache.polaris.immutables.PolarisImmutable;
29+
30+
/**
31+
* Response for S3 signing requests.
32+
*
33+
* <p>Copy of {@link org.apache.iceberg.aws.s3.signer.S3SignResponse}, because the original does not
34+
* have Jackson annotations.
35+
*/
36+
@PolarisImmutable
37+
@JsonDeserialize(as = ImmutableS3SignResponse.class)
38+
@JsonSerialize(as = ImmutableS3SignResponse.class)
39+
public interface S3SignResponse extends RESTResponse {
40+
41+
URI uri();
42+
43+
Map<String, List<String>> headers();
44+
45+
@Override
46+
default void validate() {}
47+
}

integration-tests/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation(platform(libs.iceberg.bom))
3434
implementation("org.apache.iceberg:iceberg-api")
3535
implementation("org.apache.iceberg:iceberg-core")
36+
implementation("org.apache.iceberg:iceberg-aws")
3637

3738
implementation("org.apache.iceberg:iceberg-api:${libs.versions.iceberg.get()}:tests")
3839
implementation("org.apache.iceberg:iceberg-core:${libs.versions.iceberg.get()}:tests")

polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE;
8181
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA;
8282
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES;
83+
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOTE_SIGN;
8384
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA;
8485
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES;
8586
import static org.apache.polaris.core.entity.PolarisPrivilege.VIEW_CREATE;
@@ -212,7 +213,9 @@ public enum PolarisAuthorizableOperation {
212213
GET_APPLICABLE_POLICIES_ON_TABLE(TABLE_READ_PROPERTIES),
213214
ADD_POLICY_GRANT_TO_CATALOG_ROLE(POLICY_MANAGE_GRANTS_ON_SECURABLE),
214215
REVOKE_POLICY_GRANT_FROM_CATALOG_ROLE(
215-
POLICY_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE);
216+
POLICY_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
217+
SIGN_S3_REQUEST(TABLE_REMOTE_SIGN),
218+
;
216219

217220
private final EnumSet<PolarisPrivilege> privilegesOnTarget;
218221
private final EnumSet<PolarisPrivilege> privilegesOnSecondary;

polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE;
9393
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA;
9494
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES;
95+
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOTE_SIGN;
9596
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA;
9697
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES;
9798
import static org.apache.polaris.core.entity.PolarisPrivilege.VIEW_CREATE;
@@ -126,7 +127,7 @@
126127
import org.slf4j.LoggerFactory;
127128

128129
/**
129-
* Performs hierarchical resolution logic by matching the transively expanded set of grants to a
130+
* Performs hierarchical resolution logic by matching the transitively expanded set of grants to a
130131
* calling principal against the cascading permissions over the parent hierarchy of a target
131132
* Securable.
132133
*
@@ -266,6 +267,10 @@ public class PolarisAuthorizerImpl implements PolarisAuthorizer {
266267
SUPER_PRIVILEGES.putAll(
267268
VIEW_FULL_METADATA,
268269
List.of(CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, VIEW_FULL_METADATA));
270+
SUPER_PRIVILEGES.putAll(
271+
TABLE_REMOTE_SIGN,
272+
List.of(
273+
CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA, TABLE_CREATE, TABLE_FULL_METADATA));
269274

270275
// Catalog privileges
271276
SUPER_PRIVILEGES.putAll(

polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrivilege.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public enum PolarisPrivilege {
155155
PolarisEntityType.POLICY,
156156
PolarisEntitySubType.NULL_SUBTYPE,
157157
PolarisEntityType.CATALOG_ROLE),
158+
TABLE_REMOTE_SIGN(85, PolarisEntityType.NAMESPACE),
158159
;
159160

160161
/**

runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java

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

21+
import static org.apache.polaris.service.catalog.AccessDelegationMode.REMOTE_SIGNING;
2122
import static org.apache.polaris.service.catalog.AccessDelegationMode.VENDED_CREDENTIALS;
2223
import static org.apache.polaris.service.catalog.validation.IcebergPropertiesValidation.validateIcebergProperties;
2324

@@ -32,12 +33,14 @@
3233
import jakarta.ws.rs.core.HttpHeaders;
3334
import jakarta.ws.rs.core.Response;
3435
import jakarta.ws.rs.core.SecurityContext;
36+
import jakarta.ws.rs.core.UriInfo;
3537
import java.util.EnumSet;
3638
import java.util.Map;
3739
import java.util.Optional;
3840
import java.util.Set;
3941
import java.util.function.Function;
4042
import org.apache.iceberg.MetadataUpdate;
43+
import org.apache.iceberg.aws.s3.signer.S3SignResponse;
4144
import org.apache.iceberg.catalog.Namespace;
4245
import org.apache.iceberg.catalog.TableIdentifier;
4346
import org.apache.iceberg.exceptions.BadRequestException;
@@ -81,9 +84,11 @@
8184
import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
8285
import org.apache.polaris.service.http.IcebergHttpUtil;
8386
import org.apache.polaris.service.http.IfNoneMatch;
87+
import org.apache.polaris.service.storage.aws.signer.S3RequestSignerFactory;
8488
import org.apache.polaris.service.types.CommitTableRequest;
8589
import org.apache.polaris.service.types.CommitViewRequest;
8690
import org.apache.polaris.service.types.NotificationRequest;
91+
import org.apache.polaris.service.types.S3SignRequest;
8792
import org.slf4j.Logger;
8893
import org.slf4j.LoggerFactory;
8994

@@ -144,6 +149,8 @@ public class IcebergCatalogAdapter
144149
private final CatalogPrefixParser prefixParser;
145150
private final ReservedProperties reservedProperties;
146151
private final CatalogHandlerUtils catalogHandlerUtils;
152+
private final S3RequestSignerFactory s3RequestSignerFactory;
153+
private final UriInfo uriInfo;
147154

148155
@Inject
149156
public IcebergCatalogAdapter(
@@ -157,7 +164,9 @@ public IcebergCatalogAdapter(
157164
PolarisAuthorizer polarisAuthorizer,
158165
CatalogPrefixParser prefixParser,
159166
ReservedProperties reservedProperties,
160-
CatalogHandlerUtils catalogHandlerUtils) {
167+
CatalogHandlerUtils catalogHandlerUtils,
168+
S3RequestSignerFactory s3RequestSignerFactory,
169+
UriInfo uriInfo) {
161170
this.realmContext = realmContext;
162171
this.callContext = callContext;
163172
this.catalogFactory = catalogFactory;
@@ -169,6 +178,8 @@ public IcebergCatalogAdapter(
169178
this.prefixParser = prefixParser;
170179
this.reservedProperties = reservedProperties;
171180
this.catalogHandlerUtils = catalogHandlerUtils;
181+
this.s3RequestSignerFactory = s3RequestSignerFactory;
182+
this.uriInfo = uriInfo;
172183
}
173184

174185
/**
@@ -205,7 +216,9 @@ IcebergCatalogHandler newHandlerWrapper(SecurityContext securityContext, String
205216
catalogName,
206217
polarisAuthorizer,
207218
reservedProperties,
208-
catalogHandlerUtils);
219+
catalogHandlerUtils,
220+
s3RequestSignerFactory,
221+
uriInfo);
209222
}
210223

211224
@Override
@@ -323,11 +336,13 @@ public Response updateProperties(
323336
catalog -> Response.ok(catalog.updateNamespaceProperties(ns, revisedRequest)).build());
324337
}
325338

326-
private EnumSet<AccessDelegationMode> parseAccessDelegationModes(String accessDelegationMode) {
339+
private static Set<AccessDelegationMode> parseAccessDelegationModes(String accessDelegationMode) {
327340
EnumSet<AccessDelegationMode> delegationModes =
328341
AccessDelegationMode.fromProtocolValuesList(accessDelegationMode);
329342
Preconditions.checkArgument(
330-
delegationModes.isEmpty() || delegationModes.contains(VENDED_CREDENTIALS),
343+
delegationModes.isEmpty()
344+
|| delegationModes.contains(VENDED_CREDENTIALS)
345+
|| delegationModes.contains(REMOTE_SIGNING),
331346
"Unsupported access delegation mode: %s",
332347
accessDelegationMode);
333348
return delegationModes;
@@ -342,8 +357,7 @@ public Response createTable(
342357
RealmContext realmContext,
343358
SecurityContext securityContext) {
344359
validateIcebergProperties(callContext, createTableRequest.properties());
345-
EnumSet<AccessDelegationMode> delegationModes =
346-
parseAccessDelegationModes(accessDelegationMode);
360+
Set<AccessDelegationMode> delegationModes = parseAccessDelegationModes(accessDelegationMode);
347361
Namespace ns = decodeNamespace(namespace);
348362
return withCatalog(
349363
securityContext,
@@ -354,7 +368,8 @@ public Response createTable(
354368
return Response.ok(catalog.createTableStaged(ns, createTableRequest)).build();
355369
} else {
356370
return Response.ok(
357-
catalog.createTableStagedWithWriteDelegation(ns, createTableRequest))
371+
catalog.createTableStagedWithWriteDelegation(
372+
prefix, ns, createTableRequest, delegationModes))
358373
.build();
359374
}
360375
} else if (delegationModes.isEmpty()) {
@@ -364,7 +379,8 @@ public Response createTable(
364379
.build();
365380
} else {
366381
LoadTableResponse response =
367-
catalog.createTableDirectWithWriteDelegation(ns, createTableRequest);
382+
catalog.createTableDirectWithWriteDelegation(
383+
prefix, ns, createTableRequest, delegationModes);
368384
return tryInsertETagHeader(
369385
Response.ok(response), response, namespace, createTableRequest.name())
370386
.build();
@@ -397,8 +413,7 @@ public Response loadTable(
397413
String snapshots,
398414
RealmContext realmContext,
399415
SecurityContext securityContext) {
400-
EnumSet<AccessDelegationMode> delegationModes =
401-
parseAccessDelegationModes(accessDelegationMode);
416+
Set<AccessDelegationMode> delegationModes = parseAccessDelegationModes(accessDelegationMode);
402417
Namespace ns = decodeNamespace(namespace);
403418
TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table));
404419

@@ -422,7 +437,8 @@ public Response loadTable(
422437
} else {
423438
response =
424439
catalog
425-
.loadTableWithAccessDelegationIfStale(tableIdentifier, ifNoneMatch, snapshots)
440+
.loadTableWithAccessDelegationIfStale(
441+
prefix, tableIdentifier, ifNoneMatch, delegationModes)
426442
.orElseThrow(() -> new WebApplicationException(Response.Status.NOT_MODIFIED));
427443
}
428444

@@ -588,8 +604,7 @@ public Response loadCredentials(
588604
securityContext,
589605
prefix,
590606
catalog -> {
591-
LoadTableResponse loadTableResponse =
592-
catalog.loadTableWithAccessDelegation(tableIdentifier, "all");
607+
LoadTableResponse loadTableResponse = catalog.loadTable(tableIdentifier, "all");
593608
return Response.ok(
594609
ImmutableLoadCredentialsResponse.builder()
595610
.credentials(loadTableResponse.credentials())
@@ -797,4 +812,24 @@ public Response getConfig(
797812
.build())
798813
.build();
799814
}
815+
816+
@Override
817+
public Response signS3Request(
818+
String prefix,
819+
String namespace,
820+
String table,
821+
S3SignRequest s3SignRequest,
822+
RealmContext realmContext,
823+
SecurityContext securityContext) {
824+
Namespace ns = decodeNamespace(namespace);
825+
TableIdentifier tableIdentifier = TableIdentifier.of(ns, RESTUtil.decodeString(table));
826+
return withCatalog(
827+
securityContext,
828+
prefix,
829+
catalog -> {
830+
// TODO Cache-Control header
831+
S3SignResponse response = catalog.signS3Request(s3SignRequest, tableIdentifier);
832+
return Response.status(Response.Status.OK).entity(response).build();
833+
});
834+
}
800835
}

0 commit comments

Comments
 (0)