This is an automated email from the ASF dual-hosted git repository. emaynard pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push: new 45df8ac4c Improve the parsing and validation of UserSecretReferenceUrns (#1840) 45df8ac4c is described below commit 45df8ac4cc284d9c238d8d896e6656372822cbb0 Author: Pooja Nilangekar <poo...@umd.edu> AuthorDate: Wed Jun 18 16:05:59 2025 -0400 Improve the parsing and validation of UserSecretReferenceUrns (#1840) This change addresses all the TODOs found the org.polaris.core.secrets package. Main changes: - Create a helper to parse, validate and build the URN strings. - Use Regex instead of `String.split()`. - Add Precondition checks to ensure that the URN is valid and the UserSecretManager matches the expected type. - Remove the now unused `GLOBAL_INSTANCE` of the UnsafeInMemorySecretsManager. Testing - Existing `UnsafeInMemorySecretsManagerTest` captures most of the functional changes. - Added `UserSecretReferenceUrnHelperTest` to capture the utilities exposed. --- .../core/secrets/UnsafeInMemorySecretsManager.java | 21 ++-- .../polaris/core/secrets/UserSecretReference.java | 112 +++++++++++++++++++-- .../polaris/core/secrets/UserSecretsManager.java | 13 +++ .../core/secrets/UserSecretReferenceTest.java | 83 +++++++++++++++ 4 files changed, 213 insertions(+), 16 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UnsafeInMemorySecretsManager.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UnsafeInMemorySecretsManager.java index 38cf00e22..bb690a158 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UnsafeInMemorySecretsManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UnsafeInMemorySecretsManager.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.core.secrets; +import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; @@ -34,9 +35,6 @@ import org.apache.polaris.core.entity.PolarisEntityCore; * development purposes. */ public class UnsafeInMemorySecretsManager implements UserSecretsManager { - // TODO: Remove this and wire into QuarkusProducers; just a placeholder for now to get the - // rest of the logic working. - public static final UserSecretsManager GLOBAL_INSTANCE = new UnsafeInMemorySecretsManager(); private final Map<String, String> rawSecretStore = new ConcurrentHashMap<>(); private final SecureRandom rand = new SecureRandom(); @@ -45,6 +43,8 @@ public class UnsafeInMemorySecretsManager implements UserSecretsManager { private static final String CIPHERTEXT_HASH = "ciphertext-hash"; private static final String ENCRYPTION_KEY = "encryption-key"; + public static final String SECRET_MANAGER_TYPE = "unsafe-in-memory"; + /** {@inheritDoc} */ @Override @Nonnull @@ -73,10 +73,8 @@ public class UnsafeInMemorySecretsManager implements UserSecretsManager { String secretUrn; for (int secretOrdinal = 0; ; ++secretOrdinal) { - secretUrn = - String.format( - "urn:polaris-secret:unsafe-in-memory:%d:%d", forEntity.getId(), secretOrdinal); - + String typeSpecificIdentifier = forEntity.getId() + ":" + secretOrdinal; + secretUrn = buildUrn(SECRET_MANAGER_TYPE, typeSpecificIdentifier); // Store the base64-encoded encrypted ciphertext in the simulated "secret store". String existingSecret = rawSecretStore.putIfAbsent(secretUrn, encryptedSecretCipherTextBase64); @@ -107,7 +105,14 @@ public class UnsafeInMemorySecretsManager implements UserSecretsManager { @Override @Nonnull public String readSecret(@Nonnull UserSecretReference secretReference) { - // TODO: Precondition checks and/or wire in PolarisDiagnostics + String secretManagerType = secretReference.getUserSecretManagerType(); + Preconditions.checkState( + secretManagerType.equals(SECRET_MANAGER_TYPE), + "Invalid secret manager type, expected: " + + SECRET_MANAGER_TYPE + + " got: " + + secretManagerType); + String encryptedSecretCipherTextBase64 = rawSecretStore.get(secretReference.getUrn()); if (encryptedSecretCipherTextBase64 == null) { // Secret at this URN no longer exists. 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 7181acb04..77a69dc6f 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 @@ -27,6 +27,8 @@ import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Represents a "wrapped reference" to a user-owned secret that holds an identifier to retrieve @@ -56,6 +58,43 @@ public class UserSecretReference { @JsonProperty(value = "referencePayload") private final Map<String, String> referencePayload; + private static final String URN_SCHEME = "urn"; + private static final String URN_NAMESPACE = "polaris-secret"; + private static final String SECRET_MANAGER_TYPE_REGEX = "([a-zA-Z0-9_-]+)"; + private static final String TYPE_SPECIFIC_IDENTIFIER_REGEX = + "([a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*)"; + + /** + * Precompiled regex pattern for validating the secret manager type and type-specific identifier. + */ + private static final Pattern SECRET_MANAGER_TYPE_PATTERN = + Pattern.compile("^" + SECRET_MANAGER_TYPE_REGEX + "$"); + + private static final Pattern TYPE_SPECIFIC_IDENTIFIER_PATTERN = + Pattern.compile("^" + TYPE_SPECIFIC_IDENTIFIER_REGEX + "$"); + + /** + * Precompiled regex pattern for validating and parsing UserSecretReference URNs. Expected format: + * urn:polaris-secret:<secret-manager-type>:<identifier1>(:<identifier2>:...). + * + * <p>Groups: + * + * <p>Group 1: secret-manager-type (alphanumeric, hyphens, underscores). + * + * <p>Group 2: type-specific-identifier (one or more colon-separated alphanumeric components). + */ + private static final Pattern URN_PATTERN = + Pattern.compile( + "^" + + URN_SCHEME + + ":" + + URN_NAMESPACE + + ":" + + SECRET_MANAGER_TYPE_REGEX + + ":" + + TYPE_SPECIFIC_IDENTIFIER_REGEX + + "$"); + /** * @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,16 +109,60 @@ public class UserSecretReference { public UserSecretReference( @JsonProperty(value = "urn", required = true) @Nonnull String urn, @JsonProperty(value = "referencePayload") @Nullable Map<String, String> 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:<secret-manager-type>:<type-specific-identifier>'", - urn); + urnIsValid(urn), + "Invalid secret URN: " + urn + "; must be of the form: " + URN_PATTERN.toString()); this.urn = urn; this.referencePayload = Objects.requireNonNullElse(referencePayload, new HashMap<>()); } + /** + * Validates whether the given URN string matches the expected format for UserSecretReference + * URNs. + * + * @param urn The URN string to validate. + * @return true if the URN is valid, false otherwise. + */ + private static boolean urnIsValid(@Nonnull String urn) { + return urn.trim().isEmpty() ? false : URN_PATTERN.matcher(urn).matches(); + } + + /** + * Builds a URN string from the given secret manager type and type-specific identifier. Validates + * the inputs to ensure they conform to the expected pattern. + * + * @param secretManagerType The secret manager type (alphanumeric, hyphens, underscores). + * @param typeSpecificIdentifier The type-specific identifier (colon-separated alphanumeric + * components). + * @return The constructed URN string. + */ + @Nonnull + public static String buildUrnString( + @Nonnull String secretManagerType, @Nonnull String typeSpecificIdentifier) { + + Preconditions.checkArgument( + !secretManagerType.trim().isEmpty(), "Secret manager type cannot be empty"); + Preconditions.checkArgument( + SECRET_MANAGER_TYPE_PATTERN.matcher(secretManagerType).matches(), + "Invalid secret manager type '%s'; must contain only alphanumeric characters, hyphens, and underscores", + secretManagerType); + + Preconditions.checkArgument( + !typeSpecificIdentifier.trim().isEmpty(), "Type-specific identifier cannot be empty"); + Preconditions.checkArgument( + TYPE_SPECIFIC_IDENTIFIER_PATTERN.matcher(typeSpecificIdentifier).matches(), + "Invalid type-specific identifier '%s'; must be colon-separated alphanumeric components (hyphens and underscores allowed)", + typeSpecificIdentifier); + + return URN_SCHEME + + ":" + + URN_NAMESPACE + + ":" + + secretManagerType + + ":" + + typeSpecificIdentifier; + } + /** * Since UserSecretReference objects are specific to UserSecretManager implementations, the * "secret-manager-type" portion of the URN should be used to validate that a URN is valid for a @@ -87,9 +170,22 @@ public class UserSecretReference { * concurrent implementations are possible in a given runtime environment. */ @JsonIgnore - public String getUserSecretManagerTypeFromUrn() { - // TODO: Add better/standardized parsing and validation of URN syntax - return urn.split(":")[2]; + public String getUserSecretManagerType() { + Matcher matcher = URN_PATTERN.matcher(urn); + Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); + return matcher.group(1); + } + + /** + * Returns the type-specific identifier from the URN. Since the format is specific to the + * UserSecretManager implementation, this method does not validate the identifier. It is the + * responsibility of the caller to validate it. + */ + @JsonIgnore + public String getTypeSpecificIdentifier() { + Matcher matcher = URN_PATTERN.matcher(urn); + Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); + return matcher.group(2); } public @Nonnull String getUrn() { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretsManager.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretsManager.java index b1418efc9..194223053 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretsManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretsManager.java @@ -63,4 +63,17 @@ public interface UserSecretsManager { * @param secretReference Reference object for retrieving the original secret */ void deleteSecret(@Nonnull UserSecretReference secretReference); + + /** + * Builds a URN string from the given secret manager type and type-specific identifier. + * + * @param typeSpecificIdentifier The type-specific identifier (colon-separated alphanumeric + * components with underscores and hyphens). + * @return The constructed URN string. + */ + @Nonnull + default String buildUrn( + @Nonnull String secretManagerType, @Nonnull String typeSpecificIdentifier) { + return UserSecretReference.buildUrnString(secretManagerType, typeSpecificIdentifier); + } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.java b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.java new file mode 100644 index 000000000..e05bd947b --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.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.secrets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class UserSecretReferenceTest { + + @ParameterizedTest + @ValueSource( + strings = { + "urn:polaris-secret:unsafe-in-memory:key1", + "urn:polaris-secret:unsafe-in-memory:key1:value1", + "urn:polaris-secret:aws_secrets-manager:my-key_123", + "urn:polaris-secret:vault:project:env:service:key" + }) + public void testValidUrns(String validUrn) { + assertThat(new UserSecretReference(validUrn, null)).isNotNull(); + } + + @ParameterizedTest + @ValueSource( + strings = { + "", + " ", + "not-a-urn", + "urn:", + "urn:polaris-secret:", + "urn:polaris-secret:type:", + "wrong:polaris-secret:type:key:", + "urn:polaris-secret:type with spaces:key", + "urn:polaris-secret:type@invalid:key", + "urn:polaris-secret:unsafe-in-memory:key::" + }) + public void testInvalidUrns(String invalidUrn) { + assertThatThrownBy(() -> new UserSecretReference(invalidUrn, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid secret URN: " + invalidUrn); + } + + @Test + public void tesGetUrnComponents() { + String urn = "urn:polaris-secret:unsafe-in-memory:key1:value1"; + UserSecretReference reference = new UserSecretReference(urn, null); + + assertThat(reference.getUserSecretManagerType()).isEqualTo("unsafe-in-memory"); + assertThat(reference.getTypeSpecificIdentifier()).isEqualTo("key1:value1"); + } + + @Test + public void testBuildUrn() { + String urn = UserSecretReference.buildUrnString("aws-secrets", "my-key"); + assertThat(urn).isEqualTo("urn:polaris-secret:aws-secrets:my-key"); + + String urnWithMultipleIdentifiers = + UserSecretReference.buildUrnString("vault", "project:service"); + assertThat(urnWithMultipleIdentifiers).isEqualTo("urn:polaris-secret:vault:project:service"); + + String urnWithNumbers = UserSecretReference.buildUrnString("type_123", "456:789"); + assertThat(urnWithNumbers).isEqualTo("urn:polaris-secret:type_123:456:789"); + } +}