From 4643e9705adb2703e2bd437096c2b825bd5d1d7b Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 16 Jun 2025 03:17:33 -0700 Subject: [PATCH] Add SigV4 related DPOs --- .../AuthenticationParametersDpo.java | 23 ++- .../core/connection/AuthenticationType.java | 3 +- .../BearerAuthenticationParametersDpo.java | 2 +- .../connection/ConnectionConfigInfoDpo.java | 33 ++++- .../OAuthClientCredentialsParametersDpo.java | 4 +- .../SigV4AuthenticationParametersDpo.java | 131 ++++++++++++++++++ .../hadoop/HadoopConnectionConfigInfoDpo.java | 18 ++- .../IcebergRestConnectionConfigInfoDpo.java | 19 ++- .../polaris/core/entity/CatalogEntity.java | 8 ++ .../core/identity/ServiceIdentityType.java | 83 +++++++++++ .../dpo/AwsIamServiceIdentityInfoDpo.java | 111 +++++++++++++++ .../identity/dpo/ServiceIdentityInfoDpo.java | 83 +++++++++++ .../polaris/core/secrets/SecretReference.java | 103 ++++++++++++++ .../core/secrets/ServiceSecretReference.java | 60 ++++++++ .../core/secrets/UserSecretReference.java | 59 +------- .../ConnectionConfigInfoDpoTest.java | 63 +++++++++ 16 files changed, 739 insertions(+), 64 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/ServiceIdentityType.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/secrets/SecretReference.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/secrets/ServiceSecretReference.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java index f2267b12a5..20d1e88a34 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java @@ -18,13 +18,16 @@ */ package org.apache.polaris.core.connection; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.annotation.Nonnull; import java.util.Map; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.BearerAuthenticationParameters; import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters; +import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters; import org.apache.polaris.core.connection.iceberg.IcebergCatalogPropertiesProvider; import org.apache.polaris.core.secrets.UserSecretReference; @@ -39,6 +42,7 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = OAuthClientCredentialsParametersDpo.class, name = "1"), @JsonSubTypes.Type(value = BearerAuthenticationParametersDpo.class, name = "2"), + @JsonSubTypes.Type(value = SigV4AuthenticationParametersDpo.class, name = "3"), }) public abstract class AuthenticationParametersDpo implements IcebergCatalogPropertiesProvider { @@ -57,7 +61,12 @@ public int getAuthenticationTypeCode() { return authenticationTypeCode; } - public abstract AuthenticationParameters asAuthenticationParametersModel(); + @JsonIgnore + public AuthenticationType getAuthenticationType() { + return AuthenticationType.fromCode(authenticationTypeCode); + } + + public abstract @Nonnull AuthenticationParameters asAuthenticationParametersModel(); public static AuthenticationParametersDpo fromAuthenticationParametersModelWithSecrets( AuthenticationParameters authenticationParameters, @@ -81,6 +90,18 @@ public static AuthenticationParametersDpo fromAuthenticationParametersModelWithS new BearerAuthenticationParametersDpo( secretReferences.get(INLINE_BEARER_TOKEN_REFERENCE_KEY)); break; + case SIGV4: + // SigV4 authentication is not secret-based + SigV4AuthenticationParameters sigV4AuthenticationParametersModel = + (SigV4AuthenticationParameters) authenticationParameters; + config = + new SigV4AuthenticationParametersDpo( + sigV4AuthenticationParametersModel.getRoleArn(), + sigV4AuthenticationParametersModel.getRoleSessionName(), + sigV4AuthenticationParametersModel.getExternalId(), + sigV4AuthenticationParametersModel.getSigningRegion(), + sigV4AuthenticationParametersModel.getSigningName()); + break; default: throw new IllegalStateException( "Unsupported authentication type: " + authenticationParameters.getAuthenticationType()); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java index 71ad0b72d3..2eeaae7209 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java @@ -33,6 +33,7 @@ public enum AuthenticationType { NULL_TYPE(0), OAUTH(1), BEARER(2), + SIGV4(3), ; private static final AuthenticationType[] REVERSE_MAPPING_ARRAY; @@ -65,7 +66,7 @@ public enum AuthenticationType { * NULL_TYPE if not found * * @param authTypeCode code associated to the authentication type - * @return ConnectionType corresponding to that code or null if mapping not found + * @return AuthenticationType corresponding to that code or null if mapping not found */ public static @Nonnull AuthenticationType fromCode(int authTypeCode) { // ensure it is within bounds diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java index bf80c7c4cb..49e6953752 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java @@ -56,7 +56,7 @@ public BearerAuthenticationParametersDpo( } @Override - public AuthenticationParameters asAuthenticationParametersModel() { + public @Nonnull AuthenticationParameters asAuthenticationParametersModel() { return BearerAuthenticationParameters.builder() .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.BEARER) .build(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java index 4313ede124..b2c570ffdb 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.core.connection; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -38,6 +39,7 @@ import org.apache.polaris.core.connection.hadoop.HadoopConnectionConfigInfoDpo; import org.apache.polaris.core.connection.iceberg.IcebergCatalogPropertiesProvider; import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.UserSecretReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,22 +68,29 @@ public abstract class ConnectionConfigInfoDpo implements IcebergCatalogPropertie // The authentication parameters for the connection private final AuthenticationParametersDpo authenticationParameters; + // The Polaris service identity info of the connection + private final ServiceIdentityInfoDpo serviceIdentity; + public ConnectionConfigInfoDpo( @JsonProperty(value = "connectionTypeCode", required = true) int connectionTypeCode, @JsonProperty(value = "uri", required = true) @Nonnull String uri, @JsonProperty(value = "authenticationParameters", required = true) @Nonnull - AuthenticationParametersDpo authenticationParameters) { - this(connectionTypeCode, uri, authenticationParameters, true); + AuthenticationParametersDpo authenticationParameters, + @JsonProperty(value = "serviceIdentity", required = false) @Nullable + ServiceIdentityInfoDpo serviceIdentity) { + this(connectionTypeCode, uri, authenticationParameters, serviceIdentity, true); } protected ConnectionConfigInfoDpo( int connectionTypeCode, @Nonnull String uri, @Nonnull AuthenticationParametersDpo authenticationParameters, + @Nullable ServiceIdentityInfoDpo serviceIdentity, boolean validateUri) { this.connectionTypeCode = connectionTypeCode; this.uri = uri; this.authenticationParameters = authenticationParameters; + this.serviceIdentity = serviceIdentity; if (validateUri) { validateUri(uri); } @@ -91,6 +100,11 @@ public int getConnectionTypeCode() { return connectionTypeCode; } + @JsonIgnore + public ConnectionType getConnectionType() { + return ConnectionType.fromCode(connectionTypeCode); + } + public String getUri() { return uri; } @@ -99,6 +113,10 @@ public AuthenticationParametersDpo getAuthenticationParameters() { return authenticationParameters; } + public @Nullable ServiceIdentityInfoDpo getServiceIdentity() { + return serviceIdentity; + } + private static final ObjectMapper DEFAULT_MAPPER; static { @@ -157,6 +175,7 @@ public static ConnectionConfigInfoDpo fromConnectionConfigInfoModelWithSecrets( new IcebergRestConnectionConfigInfoDpo( icebergRestConfigModel.getUri(), authenticationParameters, + null /*Service Identity Info*/, icebergRestConfigModel.getRemoteCatalogName()); break; case HADOOP: @@ -169,6 +188,7 @@ public static ConnectionConfigInfoDpo fromConnectionConfigInfoModelWithSecrets( new HadoopConnectionConfigInfoDpo( hadoopConfigModel.getUri(), authenticationParameters, + null /*Service Identity Info*/, hadoopConfigModel.getWarehouse()); break; default: @@ -178,6 +198,15 @@ public static ConnectionConfigInfoDpo fromConnectionConfigInfoModelWithSecrets( return config; } + /** + * Creates a new copy of the ConnectionConfigInfoDpo with the given service identity info. + * + * @param serviceIdentityInfo The service identity info to set. + * @return A new copy of the ConnectionConfigInfoDpo with the given service identity info. + */ + public abstract ConnectionConfigInfoDpo withServiceIdentity( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); + /** * Produces the correponding API-model ConnectionConfigInfo for this persistence object; many * fields are one-to-one direct mappings, but some fields, such as secretReferences, might only be diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java index 9a955de4fd..1b89de23bd 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java @@ -86,7 +86,7 @@ public OAuthClientCredentialsParametersDpo( return clientSecretReference; } - public @Nonnull List getScopes() { + public @Nullable List getScopes() { return scopes; } @@ -115,7 +115,7 @@ public OAuthClientCredentialsParametersDpo( } @Override - public AuthenticationParameters asAuthenticationParametersModel() { + public @Nonnull AuthenticationParameters asAuthenticationParametersModel() { return OAuthClientCredentialsParameters.builder() .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.OAUTH) .setTokenUri(getTokenUri()) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java new file mode 100644 index 0000000000..1d5ca8561f --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.connection; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Map; +import org.apache.iceberg.aws.AwsProperties; +import org.apache.iceberg.rest.auth.AuthProperties; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters; +import org.apache.polaris.core.secrets.UserSecretsManager; + +/** + * The internal persistence-object counterpart to SigV4AuthenticationParameters defined in the API + * model. + */ +public class SigV4AuthenticationParametersDpo extends AuthenticationParametersDpo { + + // The aws IAM role arn assumed by polaris userArn when signing requests + @JsonProperty(value = "roleArn") + private final String roleArn; + + // The session name used when assuming the role + @JsonProperty(value = "roleSessionName") + private final String roleSessionName; + + // An optional external id used to establish a trust relationship with AWS in the trust policy + @JsonProperty(value = "externalId") + private final String externalId; + + // Region to be used by the SigV4 protocol for signing requests + @JsonProperty(value = "signingRegion") + private final String signingRegion; + + // The service name to be used by the SigV4 protocol for signing requests, the default signing + // name is "execute-api" is if not provided + @JsonProperty(value = "signingName") + private final String signingName; + + public SigV4AuthenticationParametersDpo( + @JsonProperty(value = "roleArn", required = true) String roleArn, + @JsonProperty(value = "roleSessionName", required = false) String roleSessionName, + @JsonProperty(value = "externalId", required = false) String externalId, + @JsonProperty(value = "signingRegion", required = true) String signingRegion, + @JsonProperty(value = "signingName", required = false) String signingName) { + super(AuthenticationType.SIGV4.getCode()); + this.roleArn = roleArn; + this.roleSessionName = roleSessionName; + this.externalId = externalId; + this.signingRegion = signingRegion; + this.signingName = signingName; + } + + public @Nonnull String getRoleArn() { + return roleArn; + } + + public @Nullable String getRoleSessionName() { + return roleSessionName; + } + + public @Nullable String getExternalId() { + return externalId; + } + + public @Nonnull String getSigningRegion() { + return signingRegion; + } + + public @Nullable String getSigningName() { + return signingName; + } + + @Nonnull + @Override + public Map asIcebergCatalogProperties(UserSecretsManager secretsManager) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(AuthProperties.AUTH_TYPE, AuthProperties.AUTH_TYPE_SIGV4); + builder.put(AwsProperties.REST_SIGNER_REGION, getSigningRegion()); + if (getSigningName() != null) { + builder.put(AwsProperties.REST_SIGNING_NAME, getSigningName()); + } + + // TODO: Add a credential manager to assume the role and get the aws session credentials + return builder.build(); + } + + @Override + public @Nonnull AuthenticationParameters asAuthenticationParametersModel() { + return SigV4AuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4) + .setRoleArn(getRoleArn()) + .setRoleSessionName(getRoleSessionName()) + .setExternalId(getExternalId()) + .setSigningRegion(getSigningRegion()) + .setSigningName(getSigningName()) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("authenticationTypeCode", getAuthenticationTypeCode()) + .add("roleArn", getRoleArn()) + .add("roleSessionName", getRoleSessionName()) + .add("externalId", getExternalId()) + .add("signingRegion", getSigningRegion()) + .add("signingName", getSigningName()) + .toString(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java index 5f29482c15..05104dd208 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java @@ -24,12 +24,14 @@ import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.iceberg.CatalogProperties; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.HadoopConnectionConfigInfo; import org.apache.polaris.core.connection.AuthenticationParametersDpo; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -44,8 +46,10 @@ public HadoopConnectionConfigInfoDpo( @JsonProperty(value = "uri", required = true) @Nonnull String uri, @JsonProperty(value = "authenticationParameters", required = true) @Nonnull AuthenticationParametersDpo authenticationParameters, + @JsonProperty(value = "serviceIdentity", required = false) @Nullable + ServiceIdentityInfoDpo serviceIdentityInfo, @JsonProperty(value = "warehouse", required = false) @Nullable String remoteCatalogName) { - super(ConnectionType.HADOOP.getCode(), uri, authenticationParameters); + super(ConnectionType.HADOOP.getCode(), uri, authenticationParameters, serviceIdentityInfo); this.warehouse = remoteCatalogName; } @@ -60,6 +64,7 @@ public String toString() { .add("uri", getUri()) .add("warehouse", getWarehouse()) .add("authenticationParameters", getAuthenticationParameters().toString()) + .add("serviceIdentity", getServiceIdentity()) .toString(); } @@ -75,6 +80,13 @@ public String toString() { return properties; } + @Override + public ConnectionConfigInfoDpo withServiceIdentity( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { + return new HadoopConnectionConfigInfoDpo( + getUri(), getAuthenticationParameters(), serviceIdentityInfo, getWarehouse()); + } + @Override public ConnectionConfigInfo asConnectionConfigInfoModel() { return HadoopConnectionConfigInfo.builder() @@ -83,6 +95,10 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() { .setWarehouse(getWarehouse()) .setAuthenticationParameters( getAuthenticationParameters().asAuthenticationParametersModel()) + .setServiceIdentity( + Optional.ofNullable(getServiceIdentity()) + .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel) + .orElse(null)) .build(); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java index 236dcee293..0a6e870b7f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java @@ -24,12 +24,14 @@ import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.iceberg.CatalogProperties; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; import org.apache.polaris.core.connection.AuthenticationParametersDpo; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -45,9 +47,12 @@ public IcebergRestConnectionConfigInfoDpo( @JsonProperty(value = "uri", required = true) @Nonnull String uri, @JsonProperty(value = "authenticationParameters", required = true) @Nonnull AuthenticationParametersDpo authenticationParameters, + @JsonProperty(value = "serviceIdentity", required = false) @Nullable + ServiceIdentityInfoDpo serviceIdentityInfo, @JsonProperty(value = "remoteCatalogName", required = false) @Nullable String remoteCatalogName) { - super(ConnectionType.ICEBERG_REST.getCode(), uri, authenticationParameters); + super( + ConnectionType.ICEBERG_REST.getCode(), uri, authenticationParameters, serviceIdentityInfo); this.remoteCatalogName = remoteCatalogName; } @@ -67,6 +72,13 @@ public String getRemoteCatalogName() { return properties; } + @Override + public ConnectionConfigInfoDpo withServiceIdentity( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { + return new IcebergRestConnectionConfigInfoDpo( + getUri(), getAuthenticationParameters(), serviceIdentityInfo, getRemoteCatalogName()); + } + @Override public ConnectionConfigInfo asConnectionConfigInfoModel() { return IcebergRestConnectionConfigInfo.builder() @@ -75,6 +87,10 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() { .setRemoteCatalogName(getRemoteCatalogName()) .setAuthenticationParameters( getAuthenticationParameters().asAuthenticationParametersModel()) + .setServiceIdentity( + Optional.ofNullable(getServiceIdentity()) + .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel) + .orElse(null)) .build(); } @@ -85,6 +101,7 @@ public String toString() { .add("uri", getUri()) .add("remoteCatalogName", getRemoteCatalogName()) .add("authenticationParameters", getAuthenticationParameters().toString()) + .add("serviceIdentity", getServiceIdentity()) .toString(); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java index f975ae7392..1f9b50143e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java @@ -336,6 +336,14 @@ public Builder setConnectionConfigInfoDpoWithSecrets( return this; } + public Builder setConnectionConfigInfoDpo( + @Nonnull ConnectionConfigInfoDpo connectionConfigInfoDpo) { + internalProperties.put( + PolarisEntityConstants.getConnectionConfigInfoPropertyName(), + connectionConfigInfoDpo.serialize()); + return this; + } + @Override public CatalogEntity build() { return new CatalogEntity(buildBase()); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/ServiceIdentityType.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/ServiceIdentityType.java new file mode 100644 index 0000000000..c05e4573e8 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/ServiceIdentityType.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.identity; + +import jakarta.annotation.Nonnull; +import java.util.Arrays; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; + +/** + * The internal persistence-object counterpart to ServiceIdentityTypeInfo.ServiceIdentityTypeEnum + * defined in the API model. We define integer type codes in this enum for better compatibility + * within persisted data in case the names of enum types are ever changed in place. + * + *

Important: Codes must be kept in-sync with JsonSubTypes annotated within {@link + * ServiceIdentityInfoDpo}. + */ +public enum ServiceIdentityType { + NULL_TYPE(0), + AWS_IAM(1), + ; + + private static final ServiceIdentityType[] REVERSE_MAPPING_ARRAY; + + static { + // find max array size + int maxCode = + Arrays.stream(ServiceIdentityType.values()) + .mapToInt(ServiceIdentityType::getCode) + .max() + .orElse(0); + + // allocate mapping array + REVERSE_MAPPING_ARRAY = new ServiceIdentityType[maxCode + 1]; + + // populate mapping array + for (ServiceIdentityType authType : ServiceIdentityType.values()) { + REVERSE_MAPPING_ARRAY[authType.code] = authType; + } + } + + private final int code; + + ServiceIdentityType(int code) { + this.code = code; + } + + /** + * Given the code associated with the type, return the associated ServiceIdentityType. Return + * NULL_TYPE if not found + * + * @param identityTypeCode code associated to the service identity type + * @return ServiceIdentityType corresponding to that code or null if mapping not found + */ + public static @Nonnull ServiceIdentityType fromCode(int identityTypeCode) { + // ensure it is within bounds + if (identityTypeCode < 0 || identityTypeCode >= REVERSE_MAPPING_ARRAY.length) { + return NULL_TYPE; + } + + // get value + return REVERSE_MAPPING_ARRAY[identityTypeCode]; + } + + public int getCode() { + return this.code; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java new file mode 100644 index 0000000000..2fa6cae244 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.identity.dpo; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.regex.Pattern; +import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.secrets.ServiceSecretReference; + +/** + * Persistence-layer representation of an AWS IAM service identity used by Polaris. + * + *

This class models an AWS IAM identity (either a user or role) and extends {@link + * ServiceIdentityInfoDpo}. It is typically used internally to store both the identity metadata + * (such as the IAM ARN) and a reference to the actual credential (e.g., via {@link + * ServiceSecretReference}). + * + *

Instances of this class are convertible to the public API model {@link + * AwsIamServiceIdentityInfo}. + */ +public class AwsIamServiceIdentityInfoDpo extends ServiceIdentityInfoDpo { + + // Technically, it should be ^arn:(aws|aws-cn|aws-us-gov):iam::(\d{12}):(user|role)/.+$, + @JsonIgnore + public static final String ARN_PATTERN = "^arn:(aws|aws-us-gov):iam::(\\d{12}):(user|role)/.+$"; + + /** AWS IAM role or user ARN that represents the polaris service identity */ + @JsonProperty(value = "iamArn") + private final String iamArn; + + @JsonCreator + public AwsIamServiceIdentityInfoDpo( + @JsonProperty(value = "identityInfoReference", required = false) @Nullable + ServiceSecretReference identityInfoReference, + @JsonProperty(value = "iamArn", required = true) @Nonnull String iamArn) { + this(identityInfoReference, iamArn, true); + } + + protected AwsIamServiceIdentityInfoDpo( + @Nullable ServiceSecretReference identityInfoReference, + @Nonnull String iamArn, + boolean validateArn) { + super(ServiceIdentityType.AWS_IAM.getCode(), identityInfoReference); + this.iamArn = iamArn; + if (validateArn) { + validateArn(iamArn); + } + } + + public String getIamArn() { + return iamArn; + } + + @Override + public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() { + return AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn(getIamArn()) + .build(); + } + + /** + * Validates the ARN format. Throws an exception if the ARN is invalid. + * + * @param arn The ARN to validate. + * @throws IllegalArgumentException if the ARN is invalid. + */ + public static void validateArn(String arn) { + if (arn == null || arn.isEmpty()) { + throw new IllegalArgumentException("ARN cannot be null or empty"); + } + // specifically throw errors for China + if (arn.contains("aws-cn")) { + throw new IllegalArgumentException("AWS China is temporarily not supported"); + } + if (!Pattern.matches(ARN_PATTERN, arn)) { + throw new IllegalArgumentException("Invalid role ARN format"); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("identityTypeCode", getIdentityTypeCode()) + .add("iamArn", getIamArn()) + .toString(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java new file mode 100644 index 0000000000..ff8da6166d --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.identity.dpo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.common.base.MoreObjects; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.secrets.ServiceSecretReference; + +/** + * The internal persistence-object counterpart to ServiceIdentityInfo defined in the API model. + * Important: JsonSubTypes must be kept in sync with {@link ServiceIdentityType}. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "identityTypeCode") +@JsonSubTypes({@JsonSubTypes.Type(value = AwsIamServiceIdentityInfoDpo.class, name = "1")}) +public abstract class ServiceIdentityInfoDpo { + + @JsonProperty(value = "identityTypeCode") + private final int identityTypeCode; + + @JsonProperty(value = "identityInfoReference") + private final ServiceSecretReference identityInfoReference; + + public ServiceIdentityInfoDpo( + @JsonProperty(value = "identityTypeCode", required = true) int identityTypeCode, + @JsonProperty(value = "identityInfoReference", required = false) @Nullable + ServiceSecretReference identityInfoReference) { + this.identityTypeCode = identityTypeCode; + this.identityInfoReference = identityInfoReference; + } + + public int getIdentityTypeCode() { + return identityTypeCode; + } + + @JsonIgnore + public ServiceIdentityType getIdentityType() { + return ServiceIdentityType.fromCode(identityTypeCode); + } + + @JsonProperty + public ServiceSecretReference getIdentityInfoReference() { + return identityInfoReference; + } + + /** + * Converts this persistence object to the corresponding API model. During the conversion, some + * fields will be dropped, e.g. the reference to the service identity's credential + */ + public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel(); + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("identityTypeCode", getIdentityTypeCode()) + .toString(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/SecretReference.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/SecretReference.java new file mode 100644 index 0000000000..ed95c53e12 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/SecretReference.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.secrets; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a "wrapped reference" to a secret that holds an identifier to retrieve possibly + * remotely-stored secret material, along with an open-ended "referencePayload" that is specific to + * an implementation of the secret storage and which is needed "unwrap" the actual secret in + * combination with whatever is stored in the remote secrets storage. + * + *

Example scenarios: + * + *

If an implementation simply stores secrets directly in the secrets manager, the + * referencePayload may be empty and "unwrapping" would be a simple identity/no-op transformation. + * + *

If tampering or corruption of secrets in the secrets manager presents a unique threat, an + * implementation may use the referencePayload to ensure data integrity of the secret by storing a + * checksum or hash of the stored secret. + * + *

If the system must protect against independent exfiltration/attacks on a dedicated secrets + * manager and the core persistence database, the referencePayload may be used to coordinate + * secondary encryption keys such that the original secret can only be fully "unwrapped" given both + * the stored "secret material" as well as the referencePayload and any associated keys used for + * encryption. + */ +public class SecretReference { + @JsonProperty(value = "urn") + private final String urn; + + @JsonProperty(value = "referencePayload") + private final Map referencePayload; + + /** + * @param urn A string which should be self-sufficient to retrieve whatever secret material that + * is stored in the remote secret store. + * @param referencePayload Optionally, any additional information that is necessary to fully + * reconstitute the original secret based on what is retrieved by the {@code urn}; this + * payload may include hashes/checksums, encryption key ids, OTP encryption keys, additional + * protocol/version specifiers, etc., which are implementation-specific. + */ + public SecretReference( + @JsonProperty(value = "urn", required = true) @Nonnull String urn, + @JsonProperty(value = "referencePayload") @Nullable Map referencePayload) { + this.urn = urn; + this.referencePayload = Objects.requireNonNullElse(referencePayload, new HashMap<>()); + } + + public @Nonnull String getUrn() { + return urn; + } + + public @Nonnull Map getReferencePayload() { + return referencePayload; + } + + @Override + public int hashCode() { + return Objects.hash(getUrn(), getReferencePayload()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof SecretReference)) { + return false; + } + SecretReference that = (SecretReference) obj; + return Objects.equals(this.getUrn(), that.getUrn()) + && Objects.equals(this.getReferencePayload(), that.getReferencePayload()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("urn", getUrn()) + .add("referencePayload", String.format("", getReferencePayload().size())) + .toString(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/ServiceSecretReference.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/ServiceSecretReference.java new file mode 100644 index 0000000000..a81eb3fdf6 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/ServiceSecretReference.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.secrets; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.Map; + +/** + * Represents a "wrapped reference" to a service-owned secret that holds an identifier to retrieve + * possibly remotely-stored secret material, along with an open-ended "referencePayload" that is + * specific to an implementation of the secret storage and which is needed "unwrap" the actual + * secret in combination with whatever is stored in the remote secrets storage. + */ +public class ServiceSecretReference extends SecretReference { + /** + * @param urn A string which should be self-sufficient to retrieve whatever secret material that + * is stored in the remote secret store. e.g., + * 'urn:polaris-service-secret:<service-manager-type>:<type-specific-identifier> + * @param referencePayload Optionally, any additional information that is necessary to fully + * reconstitute the original secret based on what is retrieved by the {@code urn}; this + * payload may include hashes/checksums, encryption key ids, OTP encryption keys, additional + * protocol/version specifiers, etc., which are implementation-specific. + */ + public ServiceSecretReference( + @JsonProperty(value = "urn", required = true) @Nonnull String urn, + @JsonProperty(value = "referencePayload") @Nullable Map referencePayload) { + super(urn, referencePayload); + // TODO: Add better/standardized parsing and validation of URN syntax + Preconditions.checkArgument( + urn.startsWith("urn:polaris-service-secret:") && urn.split(":").length >= 4, + "Invalid secret URN '%s'; must be of the form " + + "'urn:polaris-service-secret::'", + urn); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ServiceSecretReference && super.equals(obj); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReference.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReference.java index 7181acb041..71a7839df2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReference.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReference.java @@ -20,42 +20,18 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.HashMap; import java.util.Map; -import java.util.Objects; /** * Represents a "wrapped reference" to a user-owned secret that holds an identifier to retrieve * possibly remotely-stored secret material, along with an open-ended "referencePayload" that is * specific to an implementation of the secret storage and which is needed "unwrap" the actual * secret in combination with whatever is stored in the remote secrets storage. - * - *

Example scenarios: - * - *

If an implementation simply stores secrets directly in the secrets manager, the - * referencePayload may be empty and "unwrapping" would be a simple identity/no-op transformation. - * - *

If tampering or corruption of secrets in the secrets manager presents a unique threat, an - * implementation may use the referencePayload to ensure data integrity of the secret by storing a - * checksum or hash of the stored secret. - * - *

If the system must protect against independent exfiltration/attacks on a dedicated secrets - * manager and the core persistence database, the referencePayload may be used to coordinate - * secondary encryption keys such that the original secret can only be fully "unwrapped" given both - * the stored "secret material" as well as the referencePayload and any associated keys used for - * encryption. */ -public class UserSecretReference { - @JsonProperty(value = "urn") - private final String urn; - - @JsonProperty(value = "referencePayload") - private final Map referencePayload; - +public class UserSecretReference extends SecretReference { /** * @param urn A string which should be self-sufficient to retrieve whatever secret material that * is stored in the remote secret store and also to identify an implementation of the @@ -70,14 +46,13 @@ public class UserSecretReference { public UserSecretReference( @JsonProperty(value = "urn", required = true) @Nonnull String urn, @JsonProperty(value = "referencePayload") @Nullable Map referencePayload) { + super(urn, referencePayload); // TODO: Add better/standardized parsing and validation of URN syntax Preconditions.checkArgument( urn.startsWith("urn:polaris-secret:") && urn.split(":").length >= 4, "Invalid secret URN '%s'; must be of the form " + "'urn:polaris-secret::'", urn); - this.urn = urn; - this.referencePayload = Objects.requireNonNullElse(referencePayload, new HashMap<>()); } /** @@ -89,37 +64,11 @@ public UserSecretReference( @JsonIgnore public String getUserSecretManagerTypeFromUrn() { // TODO: Add better/standardized parsing and validation of URN syntax - return urn.split(":")[2]; - } - - public @Nonnull String getUrn() { - return urn; - } - - public @Nonnull Map getReferencePayload() { - return referencePayload; - } - - @Override - public int hashCode() { - return Objects.hash(getUrn(), getReferencePayload()); + return getUrn().split(":")[2]; } @Override public boolean equals(Object obj) { - if (obj == null || !(obj instanceof UserSecretReference)) { - return false; - } - UserSecretReference that = (UserSecretReference) obj; - return Objects.equals(this.getUrn(), that.getUrn()) - && Objects.equals(this.getReferencePayload(), that.getReferencePayload()); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("urn", getUrn()) - .add("referencePayload", String.format("", getReferencePayload().size())) - .toString(); + return obj instanceof UserSecretReference && super.equals(obj); } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java index 3aeed0b05f..d12448cc81 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java @@ -131,4 +131,67 @@ void testBearerAuthenticationParameters() throws JsonProcessingException { objectMapper.readValue(expectedApiModelJson, ConnectionConfigInfo.class), connectionConfigInfoApiModel); } + + @Test + void testSigV4AuthenticationParameters() throws JsonProcessingException { + // Test deserialization and reserialization of the persistence JSON. + String json = + "" + + "{" + + " \"connectionTypeCode\": 1," + + " \"uri\": \"https://glue.us-west-2.amazonaws.com/iceberg\"," + + " \"remoteCatalogName\": \"123456789012\"," + + " \"authenticationParameters\": {" + + " \"authenticationTypeCode\": 3," + + " \"roleArn\": \"arn:aws:iam::123456789012:role/glue-catalog-role\"," + + " \"roleSessionName\": \"polaris-catalog-federation\"," + + " \"externalId\": \"external-id\"," + + " \"signingRegion\": \"us-west-2\"," + + " \"signingName\": \"glue\"" + + " }," + + " \"serviceIdentity\": {" + + " \"identityTypeCode\": 1," + + " \"iamArn\": \"arn:aws:iam::123456789012:user/polaris-iam-user\"," + + " \"identityInfoReference\": {" + + " \"urn\": \"urn:polaris-service-secret:default-identity-registry:my-realm:AWS_IAM\"," + + " \"referencePayload\": {" + + " \"key\": \"value\"" + + " }" + + " }" + + " }" + + "}"; + + ConnectionConfigInfoDpo connectionConfigInfoDpo = + ConnectionConfigInfoDpo.deserialize(polarisDiagnostics, json); + Assertions.assertNotNull(connectionConfigInfoDpo); + JsonNode tree1 = objectMapper.readTree(json); + JsonNode tree2 = objectMapper.readTree(connectionConfigInfoDpo.serialize()); + Assertions.assertEquals(tree1, tree2); + + // Test conversion into API model JSON. + ConnectionConfigInfo connectionConfigInfoApiModel = + connectionConfigInfoDpo.asConnectionConfigInfoModel(); + String expectedApiModelJson = + "" + + "{" + + " \"connectionType\": \"ICEBERG_REST\"," + + " \"uri\": \"https://glue.us-west-2.amazonaws.com/iceberg\"," + + " \"remoteCatalogName\": \"123456789012\"," + + " \"authenticationParameters\": {" + + " \"authenticationType\": \"SIGV4\"," + + " \"roleArn\": \"arn:aws:iam::123456789012:role/glue-catalog-role\"," + + " \"roleSessionName\": \"polaris-catalog-federation\"," + + " \"externalId\": \"external-id\"," + + " \"signingRegion\": \"us-west-2\"," + + " \"signingName\": \"glue\"" + + " }," + + " \"serviceIdentity\": {" + + " \"identityType\": \"AWS_IAM\"," + + " \"iamArn\": \"arn:aws:iam::123456789012:user/polaris-iam-user\"" + + " }" + + "}"; + Assertions.assertEquals( + objectMapper.readValue(expectedApiModelJson, ConnectionConfigInfo.class), + connectionConfigInfoApiModel); + } }