Skip to content

Commit b34ee3f

Browse files
dennishuoXJDKC
andauthored
Initial MVP implementation of Catalog Federation to remote Iceberg REST Catalogs (#1305)
* Initial prototype of catalog federation just passing special properties into internal properties. Make Resolver federation-aware to properly handle "best-effort" resolution of passthrough facade entities. Targets will automatically reflect the longest-path that we happen to have stored locally and resolve grants against that path (including the degenerate case where the longest-path is just the catalog itself). This provides Catalog-level RBAC for passthrough federation. Sketch out persistence-layer flow for how connection secrets might be pushed down into a secrets-management layer. * Defined internal representation classes for connection config * Construct and initialize federated iceberg catalog based on connection config * Apply the same spec renames to the internal ConnectionConfiguration representations. * Manually pick @XJDKC fixes for integration tests and omittign secrets in response objects * Fix internal connection structs with updated naming from spec PR * Push CreateCatalogRequest down to PolarisAdminService::createCatalog just like UpdateCatalogRequest in updateCatalog. This is needed if we're going to make PolarisAdminService handle secrets management without ever putting the secrets into a CatalogEntity. * Add new interface UserSecretsManager along with a default implementation The default UnsafeInMemorySecretsManager just uses an inmemory ConcurrentHashMap to store secrets, but structurally illustrates the full flow of intended implementations. For mutual protection against a compromise of a secret store or the core persistence store, the default implementation demonstrates storing only an encrypted secret in the secret store, and a one-time-pad key in the returned referencePayload; other implementations using standard crypto protocols may choose to instead only utilize the remote secret store as the encryption keystore while storing the ciphertext in the referencePayload (e.g. using a KMS engine with Vault vs using a KV engine). Additionally, it demonstrates the use of an integrity check by storing a basic hashCode in the referencePayload as well. * Wire in UserSecretsManager to createCatalog and federated Iceberg API handlers Update the internal DPOs corresponding to the various ConnectionConfigInfo API objects to no longer contain any possible fields for inline secrets, instead holding the JSON-serializable UserSecretReference corresponding to external/offloaded secrets. CreateCatalog for federated catalogs containing secrets will now first extract UserSecretReferences from the CreateCatalogRequest, and the CatalogEntity will populate the DPOs corresponding to ConnectionConfigInfos in a secondary pass by pulling out the relevant extracted UserSecretReferences. For federated catalog requests, when reconstituting the actual sensitive secret configs, the UserSecretsManager will be used to obtain the secrets by using the stored UserSecretReferences. Remove vestigial internal properties from earlier prototypes. * Since we already use commons-codec DigestUtils.sha256Hex, use that for the hash in UnsafeInMemorySecretsManager just for consistency and to illustrate a typical scenario using a cryptographic hash. * Rename the persistence-objects corresponding to API model objects with a new naming convention that just takes the API model object name and appends "Dpo" as a suffix; * Use UserSecretsManagerFactory to Produce the UserSecretsManager (#1) * Move PolarisAuthenticationParameters to a top-level property according to the latest spec * Create a Factory for UserSecretsManager * Fix a typo in UnsafeInMemorySecretsManagerFactory * Gate all federation logic behind a new FeatureConfiguration - ENABLE_CATALOG_FEDERATION * Also rename some variables and method names to be consistent with prior rename to ConnectionConfigInfoDpo * Change ConnectionType and AuthenticationType to be stored as int codes in persistence objects. Address PR feedback for various nits and javadoc comments. * Add javadoc comment to IcebergCatalogPropertiesProvider * Add some constraints on the expected format of the URN in UserSecretReference and placeholders for next steps where we'd provide a ResolvingUserSecretsManager for example if the runtime ever needs to delegate to two different implementations of UserSecretsManager for different entities. Reduce the `forEntity` argument to just PolarisEntityCore to make it more clear that the implementation is supposed to extract the necessary identifier info from forEntity for backend cleanup and tracking purposes. --------- Co-authored-by: Rulin Xing <rulin.xing+oss@snowflake.com> Co-authored-by: Rulin Xing <xjdkcsq3@gmail.com>
1 parent 475d10a commit b34ee3f

File tree

40 files changed

+1983
-131
lines changed

40 files changed

+1983
-131
lines changed

integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisApplicationIntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ public void testIcebergCreateTablesInExternalCatalog() throws IOException {
378378
.withPartitionSpec(PartitionSpec.unpartitioned())
379379
.create())
380380
.isInstanceOf(BadRequestException.class)
381-
.hasMessage("Malformed request: Cannot create table on external catalogs.");
381+
.hasMessage("Malformed request: Cannot create table on static-facade external catalogs.");
382382
}
383383
}
384384

@@ -515,7 +515,7 @@ public void testIcebergUpdateTableInExternalCatalog() throws IOException {
515515
10L))
516516
.commit())
517517
.isInstanceOf(BadRequestException.class)
518-
.hasMessage("Malformed request: Cannot update table on external catalogs.");
518+
.hasMessage("Malformed request: Cannot update table on static-facade external catalogs.");
519519
}
520520
}
521521

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Optional;
2323
import org.apache.polaris.core.admin.model.StorageConfigInfo;
24+
import org.apache.polaris.core.context.CallContext;
2425
import org.apache.polaris.core.persistence.cache.EntityWeigher;
2526

2627
/**
@@ -36,6 +37,22 @@ protected FeatureConfiguration(
3637
super(key, description, defaultValue, catalogConfig);
3738
}
3839

40+
/**
41+
* Helper for the common scenario of gating a feature with a boolean FeatureConfiguration, where
42+
* we want to throw an UnsupportedOperationException if it's not enabled.
43+
*/
44+
public static void enforceFeatureEnabledOrThrow(
45+
CallContext callContext, FeatureConfiguration<Boolean> featureConfig) {
46+
boolean enabled =
47+
callContext
48+
.getPolarisCallContext()
49+
.getConfigurationStore()
50+
.getConfiguration(callContext.getPolarisCallContext(), featureConfig);
51+
if (!enabled) {
52+
throw new UnsupportedOperationException("Feature not enabled: " + featureConfig.key);
53+
}
54+
}
55+
3956
public static final FeatureConfiguration<Boolean>
4057
ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING =
4158
PolarisConfiguration.<Boolean>builder()
@@ -201,4 +218,13 @@ protected FeatureConfiguration(
201218
+ " requires experimentation in the specific deployment environment")
202219
.defaultValue(100 * EntityWeigher.WEIGHT_PER_MB)
203220
.buildFeatureConfiguration();
221+
222+
public static final FeatureConfiguration<Boolean> ENABLE_CATALOG_FEDERATION =
223+
PolarisConfiguration.<Boolean>builder()
224+
.key("ENABLE_CATALOG_FEDERATION")
225+
.description(
226+
"If true, allows creating and using ExternalCatalogs containing ConnectionConfigInfos"
227+
+ " to perform federation to remote catalogs.")
228+
.defaultValue(false)
229+
.buildFeatureConfiguration();
204230
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.connection;
20+
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.fasterxml.jackson.annotation.JsonSubTypes;
23+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
24+
import java.util.Map;
25+
import org.apache.polaris.core.admin.model.AuthenticationParameters;
26+
import org.apache.polaris.core.admin.model.BearerAuthenticationParameters;
27+
import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters;
28+
import org.apache.polaris.core.secrets.UserSecretReference;
29+
30+
/**
31+
* The internal persistence-object counterpart to AuthenticationParameters defined in the API model.
32+
* Important: JsonSubTypes must be kept in sync with {@link AuthenticationType}.
33+
*/
34+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "authenticationTypeCode", visible = true)
35+
@JsonSubTypes({
36+
@JsonSubTypes.Type(value = OAuthClientCredentialsParametersDpo.class, name = "1"),
37+
@JsonSubTypes.Type(value = BearerAuthenticationParametersDpo.class, name = "2"),
38+
})
39+
public abstract class AuthenticationParametersDpo implements IcebergCatalogPropertiesProvider {
40+
41+
public static final String INLINE_CLIENT_SECRET_REFERENCE_KEY = "inlineClientSecretReference";
42+
public static final String INLINE_BEARER_TOKEN_REFERENCE_KEY = "inlineBearerTokenReference";
43+
44+
@JsonProperty(value = "authenticationTypeCode")
45+
private final int authenticationTypeCode;
46+
47+
public AuthenticationParametersDpo(
48+
@JsonProperty(value = "authenticationTypeCode", required = true) int authenticationTypeCode) {
49+
this.authenticationTypeCode = authenticationTypeCode;
50+
}
51+
52+
public int getAuthenticationTypeCode() {
53+
return authenticationTypeCode;
54+
}
55+
56+
public abstract AuthenticationParameters asAuthenticationParametersModel();
57+
58+
public static AuthenticationParametersDpo fromAuthenticationParametersModelWithSecrets(
59+
AuthenticationParameters authenticationParameters,
60+
Map<String, UserSecretReference> secretReferences) {
61+
final AuthenticationParametersDpo config;
62+
switch (authenticationParameters.getAuthenticationType()) {
63+
case OAUTH:
64+
OAuthClientCredentialsParameters oauthClientCredentialsModel =
65+
(OAuthClientCredentialsParameters) authenticationParameters;
66+
config =
67+
new OAuthClientCredentialsParametersDpo(
68+
AuthenticationType.OAUTH.getCode(),
69+
oauthClientCredentialsModel.getTokenUri(),
70+
oauthClientCredentialsModel.getClientId(),
71+
secretReferences.get(INLINE_CLIENT_SECRET_REFERENCE_KEY),
72+
oauthClientCredentialsModel.getScopes());
73+
break;
74+
case BEARER:
75+
BearerAuthenticationParameters bearerAuthenticationParametersModel =
76+
(BearerAuthenticationParameters) authenticationParameters;
77+
config =
78+
new BearerAuthenticationParametersDpo(
79+
AuthenticationType.BEARER.getCode(),
80+
secretReferences.get(INLINE_BEARER_TOKEN_REFERENCE_KEY));
81+
break;
82+
default:
83+
throw new IllegalStateException(
84+
"Unsupported authentication type: " + authenticationParameters.getAuthenticationType());
85+
}
86+
return config;
87+
}
88+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.connection;
20+
21+
/**
22+
* The internal persistence-object counterpart to AuthenticationParameters.AuthenticationTypeEnum
23+
* defined in the API model. We define integer type codes in this enum for better compatibility
24+
* within persisted data in case the names of enum types are ever changed in place.
25+
*
26+
* <p>Important: Codes must be kept in-sync with JsonSubTypes annotated within {@link
27+
* AuthenticationParametersDpo}.
28+
*/
29+
public enum AuthenticationType {
30+
OAUTH(1),
31+
BEARER(2);
32+
33+
private final int code;
34+
35+
AuthenticationType(int code) {
36+
this.code = code;
37+
}
38+
39+
public int getCode() {
40+
return this.code;
41+
}
42+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.connection;
20+
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.google.common.base.MoreObjects;
23+
import jakarta.annotation.Nonnull;
24+
import java.util.Map;
25+
import org.apache.iceberg.rest.auth.OAuth2Properties;
26+
import org.apache.polaris.core.admin.model.AuthenticationParameters;
27+
import org.apache.polaris.core.admin.model.BearerAuthenticationParameters;
28+
import org.apache.polaris.core.secrets.UserSecretReference;
29+
import org.apache.polaris.core.secrets.UserSecretsManager;
30+
31+
/**
32+
* The internal persistence-object counterpart to BearerAuthenticationParameters defined in the API
33+
* model.
34+
*/
35+
public class BearerAuthenticationParametersDpo extends AuthenticationParametersDpo {
36+
37+
@JsonProperty(value = "bearerTokenReference")
38+
private final UserSecretReference bearerTokenReference;
39+
40+
public BearerAuthenticationParametersDpo(
41+
@JsonProperty(value = "authenticationTypeCode", required = true) int authenticationTypeCode,
42+
@JsonProperty(value = "bearerTokenReference", required = true) @Nonnull
43+
UserSecretReference bearerTokenReference) {
44+
super(authenticationTypeCode);
45+
this.bearerTokenReference = bearerTokenReference;
46+
}
47+
48+
public @Nonnull UserSecretReference getBearerTokenReference() {
49+
return bearerTokenReference;
50+
}
51+
52+
@Override
53+
public @Nonnull Map<String, String> asIcebergCatalogProperties(
54+
UserSecretsManager secretsManager) {
55+
String bearerToken = secretsManager.readSecret(getBearerTokenReference());
56+
return Map.of(OAuth2Properties.TOKEN, bearerToken);
57+
}
58+
59+
@Override
60+
public AuthenticationParameters asAuthenticationParametersModel() {
61+
return BearerAuthenticationParameters.builder()
62+
.setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.BEARER)
63+
.build();
64+
}
65+
66+
@Override
67+
public String toString() {
68+
return MoreObjects.toStringHelper(this)
69+
.add("authenticationTypeCode", getAuthenticationTypeCode())
70+
.add("bearerTokenReference", getBearerTokenReference())
71+
.toString();
72+
}
73+
}

0 commit comments

Comments
 (0)