Skip to content

Commit 793a118

Browse files
authored
SigV4 Auth Support for Catalog Federation - Part 3: Service Identity Info Injection (#2523)
This PR introduces service identity management for SigV4 Auth Support for Catalog Federation. Unlike user-supplied parameters, the service identity represents the identity of the Polaris service itself and should be managed by Polaris. * Service Identity Injection * Return injected service identity info in response * Use AwsCredentialsProvider to retrieve the credentials * Move some logic to ServiceIdentityConfiguration * Rename ServiceIdentityRegistry to ServiceIdentityProvider * Rename ResolvedServiceIdentity to ServiceIdentityCredential * Simplify the logic and add more tests * Use SecretReference and fix some small issues * Disable Catalog Federation
1 parent 3e80675 commit 793a118

File tree

34 files changed

+1663
-73
lines changed

34 files changed

+1663
-73
lines changed

polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ public static void enforceFeatureEnabledOrThrow(
303303
.defaultValue(
304304
List.of(
305305
AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name(),
306-
AuthenticationParameters.AuthenticationTypeEnum.BEARER.name()))
306+
AuthenticationParameters.AuthenticationTypeEnum.BEARER.name(),
307+
AuthenticationParameters.AuthenticationTypeEnum.SIGV4.name()))
307308
.buildFeatureConfiguration();
308309

309310
public static final FeatureConfiguration<Integer> ICEBERG_COMMIT_MAX_RETRIES =

polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.apache.polaris.core.connection.iceberg.IcebergCatalogPropertiesProvider;
4242
import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
4343
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
44+
import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
4445
import org.apache.polaris.core.secrets.SecretReference;
4546
import org.slf4j.Logger;
4647
import org.slf4j.LoggerFactory;
@@ -225,9 +226,10 @@ public abstract ConnectionConfigInfoDpo withServiceIdentity(
225226
@Nonnull ServiceIdentityInfoDpo serviceIdentityInfo);
226227

227228
/**
228-
* Produces the correponding API-model ConnectionConfigInfo for this persistence object; many
229+
* Produces the corresponding API-model ConnectionConfigInfo for this persistence object; many
229230
* fields are one-to-one direct mappings, but some fields, such as secretReferences, might only be
230231
* applicable/present in the persistence object, but not the API model object.
231232
*/
232-
public abstract ConnectionConfigInfo asConnectionConfigInfoModel();
233+
public abstract ConnectionConfigInfo asConnectionConfigInfoModel(
234+
ServiceIdentityProvider serviceIdentityProvider);
233235
}

polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
3333
import org.apache.polaris.core.connection.ConnectionType;
3434
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
35+
import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
3536
import org.apache.polaris.core.secrets.UserSecretsManager;
3637

3738
/**
@@ -88,7 +89,8 @@ public ConnectionConfigInfoDpo withServiceIdentity(
8889
}
8990

9091
@Override
91-
public ConnectionConfigInfo asConnectionConfigInfoModel() {
92+
public ConnectionConfigInfo asConnectionConfigInfoModel(
93+
ServiceIdentityProvider serviceIdentityProvider) {
9294
return HadoopConnectionConfigInfo.builder()
9395
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HADOOP)
9496
.setUri(getUri())
@@ -97,7 +99,9 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() {
9799
getAuthenticationParameters().asAuthenticationParametersModel())
98100
.setServiceIdentity(
99101
Optional.ofNullable(getServiceIdentity())
100-
.map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel)
102+
.map(
103+
serviceIdentityInfoDpo ->
104+
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
101105
.orElse(null))
102106
.build();
103107
}

polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
import jakarta.annotation.Nullable;
2525
import java.util.HashMap;
2626
import java.util.Map;
27+
import java.util.Optional;
2728
import org.apache.iceberg.CatalogProperties;
2829
import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
2930
import org.apache.polaris.core.admin.model.HiveConnectionConfigInfo;
3031
import org.apache.polaris.core.connection.AuthenticationParametersDpo;
3132
import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
3233
import org.apache.polaris.core.connection.ConnectionType;
3334
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
35+
import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
3436
import org.apache.polaris.core.secrets.UserSecretsManager;
3537

3638
/**
@@ -88,13 +90,20 @@ public ConnectionConfigInfoDpo withServiceIdentity(
8890
}
8991

9092
@Override
91-
public ConnectionConfigInfo asConnectionConfigInfoModel() {
93+
public ConnectionConfigInfo asConnectionConfigInfoModel(
94+
ServiceIdentityProvider serviceIdentityProvider) {
9295
return HiveConnectionConfigInfo.builder()
9396
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HIVE)
9497
.setUri(getUri())
9598
.setWarehouse(getWarehouse())
9699
.setAuthenticationParameters(
97100
getAuthenticationParameters().asAuthenticationParametersModel())
101+
.setServiceIdentity(
102+
Optional.ofNullable(getServiceIdentity())
103+
.map(
104+
serviceIdentityInfoDpo ->
105+
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
106+
.orElse(null))
98107
.build();
99108
}
100109
}

polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
3333
import org.apache.polaris.core.connection.ConnectionType;
3434
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
35+
import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
3536
import org.apache.polaris.core.secrets.UserSecretsManager;
3637

3738
/**
@@ -80,7 +81,8 @@ public ConnectionConfigInfoDpo withServiceIdentity(
8081
}
8182

8283
@Override
83-
public ConnectionConfigInfo asConnectionConfigInfoModel() {
84+
public ConnectionConfigInfo asConnectionConfigInfoModel(
85+
ServiceIdentityProvider serviceIdentityProvider) {
8486
return IcebergRestConnectionConfigInfo.builder()
8587
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
8688
.setUri(getUri())
@@ -89,7 +91,9 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() {
8991
getAuthenticationParameters().asAuthenticationParametersModel())
9092
.setServiceIdentity(
9193
Optional.ofNullable(getServiceIdentity())
92-
.map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel)
94+
.map(
95+
serviceIdentityInfoDpo ->
96+
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
9397
.orElse(null))
9498
.build();
9599
}

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import org.apache.polaris.core.config.BehaviorChangeConfiguration;
4444
import org.apache.polaris.core.config.RealmConfig;
4545
import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
46+
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
47+
import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
4648
import org.apache.polaris.core.secrets.SecretReference;
4749
import org.apache.polaris.core.storage.FileStorageConfigurationInfo;
4850
import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -108,6 +110,10 @@ public static CatalogEntity fromCatalog(RealmConfig realmConfig, Catalog catalog
108110
}
109111

110112
public Catalog asCatalog() {
113+
return this.asCatalog(null);
114+
}
115+
116+
public Catalog asCatalog(ServiceIdentityProvider serviceIdentityProvider) {
111117
Map<String, String> internalProperties = getInternalPropertiesAsMap();
112118
Catalog.TypeEnum catalogType =
113119
Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY))
@@ -118,6 +124,12 @@ public Catalog asCatalog() {
118124
CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY))
119125
.putAll(propertiesMap)
120126
.build();
127+
128+
// Right now, only external catalog may use ServiceIdentityProvider to resolve identity
129+
Preconditions.checkState(
130+
catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityProvider != null,
131+
"%s catalog needs ServiceIdentityProvider to resolve service identities",
132+
Catalog.TypeEnum.EXTERNAL);
121133
return catalogType == Catalog.TypeEnum.EXTERNAL
122134
? ExternalCatalog.builder()
123135
.setType(Catalog.TypeEnum.EXTERNAL)
@@ -127,7 +139,7 @@ public Catalog asCatalog() {
127139
.setLastUpdateTimestamp(getLastUpdateTimestamp())
128140
.setEntityVersion(getEntityVersion())
129141
.setStorageConfigInfo(getStorageInfo(internalProperties))
130-
.setConnectionConfigInfo(getConnectionInfo(internalProperties))
142+
.setConnectionConfigInfo(getConnectionInfo(internalProperties, serviceIdentityProvider))
131143
.build()
132144
: PolarisCatalog.builder()
133145
.setType(Catalog.TypeEnum.INTERNAL)
@@ -187,11 +199,12 @@ private StorageConfigInfo getStorageInfo(Map<String, String> internalProperties)
187199
return null;
188200
}
189201

190-
private ConnectionConfigInfo getConnectionInfo(Map<String, String> internalProperties) {
202+
private ConnectionConfigInfo getConnectionInfo(
203+
Map<String, String> internalProperties, ServiceIdentityProvider serviceIdentityProvider) {
191204
if (internalProperties.containsKey(
192205
PolarisEntityConstants.getConnectionConfigInfoPropertyName())) {
193206
ConnectionConfigInfoDpo configInfo = getConnectionConfigInfoDpo();
194-
return configInfo.asConnectionConfigInfoModel();
207+
return configInfo.asConnectionConfigInfoModel(serviceIdentityProvider);
195208
}
196209
return null;
197210
}
@@ -352,11 +365,13 @@ private void validateMaxAllowedLocations(
352365

353366
public Builder setConnectionConfigInfoDpoWithSecrets(
354367
ConnectionConfigInfo connectionConfigurationModel,
355-
Map<String, SecretReference> secretReferences) {
368+
Map<String, SecretReference> secretReferences,
369+
ServiceIdentityInfoDpo serviceIdentityInfoDpo) {
356370
if (connectionConfigurationModel != null) {
357371
ConnectionConfigInfoDpo config =
358372
ConnectionConfigInfoDpo.fromConnectionConfigInfoModelWithSecrets(
359-
connectionConfigurationModel, secretReferences);
373+
connectionConfigurationModel, secretReferences)
374+
.withServiceIdentity(serviceIdentityInfoDpo);
360375
internalProperties.put(
361376
PolarisEntityConstants.getConnectionConfigInfoPropertyName(), config.serialize());
362377
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.core.identity.credential;
20+
21+
import jakarta.annotation.Nonnull;
22+
import jakarta.annotation.Nullable;
23+
import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
24+
import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
25+
import org.apache.polaris.core.identity.ServiceIdentityType;
26+
import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
27+
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
28+
import org.apache.polaris.core.secrets.SecretReference;
29+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
30+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
31+
32+
/**
33+
* Represents an AWS IAM service identity credential used by Polaris to authenticate to AWS
34+
* services.
35+
*
36+
* <p>This credential encapsulates:
37+
*
38+
* <ul>
39+
* <li>The IAM ARN (role or user) representing the Polaris service identity
40+
* <li>An {@link AwsCredentialsProvider} that supplies AWS credentials (access key, secret key,
41+
* and optional session token)
42+
* </ul>
43+
*
44+
* <p>Polaris uses this identity to assume customer-provided IAM roles when accessing remote
45+
* catalogs with SigV4 authentication. The {@link AwsCredentialsProvider} can be configured to use
46+
* either:
47+
*
48+
* <ul>
49+
* <li>Static credentials (for testing or single-tenant deployments)
50+
* <li>DefaultCredentialsProvider (which chains through various AWS credential sources)
51+
* <li>Custom credential providers (for vendor-specific secret management)
52+
* </ul>
53+
*/
54+
public class AwsIamServiceIdentityCredential extends ServiceIdentityCredential {
55+
56+
/** IAM role or user ARN representing the Polaris service identity. */
57+
private final String iamArn;
58+
59+
/** AWS credentials provider for accessing AWS services. */
60+
private final AwsCredentialsProvider awsCredentialsProvider;
61+
62+
public AwsIamServiceIdentityCredential(@Nullable String iamArn) {
63+
this(null, iamArn, DefaultCredentialsProvider.builder().build());
64+
}
65+
66+
public AwsIamServiceIdentityCredential(
67+
@Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) {
68+
this(null, iamArn, awsCredentialsProvider);
69+
}
70+
71+
public AwsIamServiceIdentityCredential(
72+
@Nullable SecretReference secretReference,
73+
@Nullable String iamArn,
74+
@Nonnull AwsCredentialsProvider awsCredentialsProvider) {
75+
super(ServiceIdentityType.AWS_IAM, secretReference);
76+
this.iamArn = iamArn;
77+
this.awsCredentialsProvider = awsCredentialsProvider;
78+
}
79+
80+
public @Nullable String getIamArn() {
81+
return iamArn;
82+
}
83+
84+
public @Nonnull AwsCredentialsProvider getAwsCredentialsProvider() {
85+
return awsCredentialsProvider;
86+
}
87+
88+
@Override
89+
public @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo() {
90+
return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference());
91+
}
92+
93+
@Override
94+
public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() {
95+
return AwsIamServiceIdentityInfo.builder()
96+
.setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
97+
.setIamArn(getIamArn())
98+
.build();
99+
}
100+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.core.identity.credential;
20+
21+
import jakarta.annotation.Nonnull;
22+
import jakarta.annotation.Nullable;
23+
import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
24+
import org.apache.polaris.core.identity.ServiceIdentityType;
25+
import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
26+
import org.apache.polaris.core.secrets.SecretReference;
27+
import software.amazon.awssdk.annotations.NotNull;
28+
29+
/**
30+
* Represents a service identity credential used by Polaris to authenticate to external systems.
31+
*
32+
* <p>This class encapsulates both the service identity metadata (e.g., AWS IAM ARN) and the
33+
* associated credentials (e.g., AWS access keys) needed to authenticate as the Polaris service when
34+
* accessing external catalog services.
35+
*
36+
* <p>The credential contains:
37+
*
38+
* <ul>
39+
* <li>Identity type (e.g., AWS_IAM)
40+
* <li>A {@link SecretReference} that serves as a unique identifier for this service identity
41+
* instance (used for lookups and persistence)
42+
* <li>The actual authentication credentials (implementation-specific, e.g.,
43+
* AwsCredentialsProvider)
44+
* </ul>
45+
*/
46+
public abstract class ServiceIdentityCredential {
47+
private final ServiceIdentityType identityType;
48+
private SecretReference identityInfoReference;
49+
50+
public ServiceIdentityCredential(@Nonnull ServiceIdentityType identityType) {
51+
this(identityType, null);
52+
}
53+
54+
public ServiceIdentityCredential(
55+
@Nonnull ServiceIdentityType identityType, @Nullable SecretReference identityInfoReference) {
56+
this.identityType = identityType;
57+
this.identityInfoReference = identityInfoReference;
58+
}
59+
60+
public @NotNull ServiceIdentityType getIdentityType() {
61+
return identityType;
62+
}
63+
64+
public @Nonnull SecretReference getIdentityInfoReference() {
65+
return identityInfoReference;
66+
}
67+
68+
public void setIdentityInfoReference(@NotNull SecretReference identityInfoReference) {
69+
this.identityInfoReference = identityInfoReference;
70+
}
71+
72+
/**
73+
* Converts this service identity credential into its corresponding persisted form (DPO).
74+
*
75+
* <p>The DPO contains only a reference to the credential, not the credential itself, as the
76+
* actual secrets are managed externally.
77+
*
78+
* @return The persistence object representation
79+
*/
80+
public abstract @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo();
81+
82+
/**
83+
* Converts this service identity credential into its API model representation.
84+
*
85+
* <p>The model contains identity information (e.g., IAM ARN) but excludes sensitive credentials
86+
* such as access keys or session tokens.
87+
*
88+
* @return The API model representation for client responses
89+
*/
90+
public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel();
91+
}

0 commit comments

Comments
 (0)