From 2a37fe0d847611fee2c037f24328ad666ef97ce1 Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Mon, 9 Jun 2025 16:46:13 -0700 Subject: [PATCH 1/3] Improve the parsing and validation of UserSecretReferenceUrns --- .../secrets/UnsafeInMemorySecretsManager.java | 22 ++- .../core/secrets/UserSecretReference.java | 13 +- .../secrets/UserSecretReferenceUrnHelper.java | 147 ++++++++++++++++++ .../UserSecretReferenceUrnHelperTest.java | 82 ++++++++++ 4 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java create mode 100644 polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java 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 38cf00e226..a096ce658d 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 @@ * 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 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"; + private static final String SECRET_MANAGER_TYPE = "unsafe-in-memory"; + /** {@inheritDoc} */ @Override @Nonnull @@ -73,9 +73,9 @@ public UserSecretReference writeSecret( String secretUrn; for (int secretOrdinal = 0; ; ++secretOrdinal) { + String typeSpecificIdentifier = forEntity.getId() + ":" + secretOrdinal; secretUrn = - String.format( - "urn:polaris-secret:unsafe-in-memory:%d:%d", forEntity.getId(), secretOrdinal); + UserSecretReferenceUrnHelper.buildUrn(SECRET_MANAGER_TYPE, typeSpecificIdentifier); // Store the base64-encoded encrypted ciphertext in the simulated "secret store". String existingSecret = @@ -107,7 +107,17 @@ public UserSecretReference writeSecret( @Override @Nonnull public String readSecret(@Nonnull UserSecretReference secretReference) { - // TODO: Precondition checks and/or wire in PolarisDiagnostics + String urn = secretReference.getUrn(); + Preconditions.checkState(UserSecretReferenceUrnHelper.isValid(urn), "Invalid URN: " + urn); + + String secretManagerType = UserSecretReferenceUrnHelper.getSecretManagerType(urn); + 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 7181acb041..147b425002 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 @@ -70,12 +70,12 @@ public class UserSecretReference { public UserSecretReference( @JsonProperty(value = "urn", required = true) @Nonnull String urn, @JsonProperty(value = "referencePayload") @Nullable Map 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); + UserSecretReferenceUrnHelper.isValid(urn), + "Invalid secret URN: " + + urn + + "; must be of the form: " + + UserSecretReferenceUrnHelper.getUrnPattern()); this.urn = urn; this.referencePayload = Objects.requireNonNullElse(referencePayload, new HashMap<>()); } @@ -88,8 +88,7 @@ public UserSecretReference( */ @JsonIgnore public String getUserSecretManagerTypeFromUrn() { - // TODO: Add better/standardized parsing and validation of URN syntax - return urn.split(":")[2]; + return UserSecretReferenceUrnHelper.getSecretManagerType(urn); } public @Nonnull String getUrn() { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java new file mode 100644 index 0000000000..0e10208f39 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java @@ -0,0 +1,147 @@ +/* + * 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.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for UserSecretReference URNs. Provides the ability to validate, parse, and build + * URNs. + */ +public final class UserSecretReferenceUrnHelper { + + private UserSecretReferenceUrnHelper() {} + + 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::(::...). + * + *

Groups: + * + *

Group 1: secret-manager-type (alphanumeric, hyphens, underscores). + * + *

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 + + "$"); + + /** + * 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. + */ + public static boolean isValid(@Nonnull String urn) { + return urn.trim().isEmpty() ? false : URN_PATTERN.matcher(urn).matches(); + } + + public static String getUrnPattern() { + return URN_PATTERN.toString(); + } + + /** + * Extracts the secret manager type from a valid URN. + * + * @param urn The URN string to parse. + * @return The secret manager type. + */ + @Nonnull + public static String getSecretManagerType(@Nonnull String urn) { + Matcher matcher = URN_PATTERN.matcher(urn); + Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); + return matcher.group(1); + } + + /** + * Extracts the type-specific identifier from a valid URN. + * + * @param urn The URN string to parse. + * @return The type-specific identifier. + */ + @Nonnull + public static String getTypeSpecificIdentifier(@Nonnull String urn) { + Matcher matcher = URN_PATTERN.matcher(urn); + Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); + return matcher.group(2); + } + + /** + * 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 buildUrn( + @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; + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java new file mode 100644 index 0000000000..1bdc3b045d --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java @@ -0,0 +1,82 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class UserSecretReferenceUrnHelperTest { + + @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(UserSecretReferenceUrnHelper.isValid(validUrn)).isTrue(); + } + + @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) { + assertThat(UserSecretReferenceUrnHelper.isValid(invalidUrn)) + .as("URN should be invalid: %s", invalidUrn) + .isFalse(); + } + + @Test + public void tesGetUrnComponents() { + String urn = "urn:polaris-secret:unsafe-in-memory:key1:value1"; + assertThat(UserSecretReferenceUrnHelper.getSecretManagerType(urn)) + .isEqualTo("unsafe-in-memory"); + assertThat(UserSecretReferenceUrnHelper.getTypeSpecificIdentifier(urn)) + .isEqualTo("key1:value1"); + } + + @Test + public void testBuildUrn() { + String urn = UserSecretReferenceUrnHelper.buildUrn("aws-secrets", "my-key"); + assertThat(urn).isEqualTo("urn:polaris-secret:aws-secrets:my-key"); + + String urnWithMultipleIdentifiers = + UserSecretReferenceUrnHelper.buildUrn("vault", "project:service"); + assertThat(urnWithMultipleIdentifiers).isEqualTo("urn:polaris-secret:vault:project:service"); + + String urnWithNumbers = UserSecretReferenceUrnHelper.buildUrn("type_123", "456:789"); + assertThat(urnWithNumbers).isEqualTo("urn:polaris-secret:type_123:456:789"); + } +} From be1ff664951ff397f180fecbc9f5fddeaf236017 Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Wed, 11 Jun 2025 15:39:27 -0700 Subject: [PATCH 2/3] Address review comments --- .../core/secrets/UnsafeInMemorySecretsManager.java | 6 ++---- .../polaris/core/secrets/UserSecretsManager.java | 13 +++++++++++++ 2 files changed, 15 insertions(+), 4 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 a096ce658d..a3f51784b4 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 @@ -43,7 +43,7 @@ public class UnsafeInMemorySecretsManager implements UserSecretsManager { private static final String CIPHERTEXT_HASH = "ciphertext-hash"; private static final String ENCRYPTION_KEY = "encryption-key"; - private static final String SECRET_MANAGER_TYPE = "unsafe-in-memory"; + public static final String SECRET_MANAGER_TYPE = "unsafe-in-memory"; /** {@inheritDoc} */ @Override @@ -74,9 +74,7 @@ public UserSecretReference writeSecret( String secretUrn; for (int secretOrdinal = 0; ; ++secretOrdinal) { String typeSpecificIdentifier = forEntity.getId() + ":" + secretOrdinal; - secretUrn = - UserSecretReferenceUrnHelper.buildUrn(SECRET_MANAGER_TYPE, typeSpecificIdentifier); - + secretUrn = buildUrn(SECRET_MANAGER_TYPE, typeSpecificIdentifier); // Store the base64-encoded encrypted ciphertext in the simulated "secret store". String existingSecret = rawSecretStore.putIfAbsent(secretUrn, encryptedSecretCipherTextBase64); 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 b1418efc9a..aa81a52bc9 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 UserSecretReferenceUrnHelper.buildUrn(secretManagerType, typeSpecificIdentifier); + } } From 3ad02bb91778ab516284b33ec5c63fccdc52e8c4 Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Fri, 13 Jun 2025 11:50:12 -0700 Subject: [PATCH 3/3] Move static functions back to UserSecretReference --- .../secrets/UnsafeInMemorySecretsManager.java | 5 +- .../core/secrets/UserSecretReference.java | 111 ++++++++++++- .../secrets/UserSecretReferenceUrnHelper.java | 147 ------------------ .../core/secrets/UserSecretsManager.java | 2 +- ...Test.java => UserSecretReferenceTest.java} | 25 +-- 5 files changed, 119 insertions(+), 171 deletions(-) delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java rename polaris-core/src/test/java/org/apache/polaris/core/secrets/{UserSecretReferenceUrnHelperTest.java => UserSecretReferenceTest.java} (73%) 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 a3f51784b4..bb690a1588 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 @@ -105,10 +105,7 @@ public UserSecretReference writeSecret( @Override @Nonnull public String readSecret(@Nonnull UserSecretReference secretReference) { - String urn = secretReference.getUrn(); - Preconditions.checkState(UserSecretReferenceUrnHelper.isValid(urn), "Invalid URN: " + urn); - - String secretManagerType = UserSecretReferenceUrnHelper.getSecretManagerType(urn); + String secretManagerType = secretReference.getUserSecretManagerType(); Preconditions.checkState( secretManagerType.equals(SECRET_MANAGER_TYPE), "Invalid secret manager type, expected: " 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 147b425002..77a69dc6f2 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 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 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::(::...). + * + *

Groups: + * + *

Group 1: secret-manager-type (alphanumeric, hyphens, underscores). + * + *

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 @@ -71,15 +110,59 @@ public UserSecretReference( @JsonProperty(value = "urn", required = true) @Nonnull String urn, @JsonProperty(value = "referencePayload") @Nullable Map referencePayload) { Preconditions.checkArgument( - UserSecretReferenceUrnHelper.isValid(urn), - "Invalid secret URN: " - + urn - + "; must be of the form: " - + UserSecretReferenceUrnHelper.getUrnPattern()); + 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,8 +170,22 @@ public UserSecretReference( * concurrent implementations are possible in a given runtime environment. */ @JsonIgnore - public String getUserSecretManagerTypeFromUrn() { - return UserSecretReferenceUrnHelper.getSecretManagerType(urn); + 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/UserSecretReferenceUrnHelper.java b/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java deleted file mode 100644 index 0e10208f39..0000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelper.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.google.common.base.Preconditions; -import jakarta.annotation.Nonnull; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Utility class for UserSecretReference URNs. Provides the ability to validate, parse, and build - * URNs. - */ -public final class UserSecretReferenceUrnHelper { - - private UserSecretReferenceUrnHelper() {} - - 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::(::...). - * - *

Groups: - * - *

Group 1: secret-manager-type (alphanumeric, hyphens, underscores). - * - *

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 - + "$"); - - /** - * 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. - */ - public static boolean isValid(@Nonnull String urn) { - return urn.trim().isEmpty() ? false : URN_PATTERN.matcher(urn).matches(); - } - - public static String getUrnPattern() { - return URN_PATTERN.toString(); - } - - /** - * Extracts the secret manager type from a valid URN. - * - * @param urn The URN string to parse. - * @return The secret manager type. - */ - @Nonnull - public static String getSecretManagerType(@Nonnull String urn) { - Matcher matcher = URN_PATTERN.matcher(urn); - Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); - return matcher.group(1); - } - - /** - * Extracts the type-specific identifier from a valid URN. - * - * @param urn The URN string to parse. - * @return The type-specific identifier. - */ - @Nonnull - public static String getTypeSpecificIdentifier(@Nonnull String urn) { - Matcher matcher = URN_PATTERN.matcher(urn); - Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn); - return matcher.group(2); - } - - /** - * 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 buildUrn( - @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; - } -} 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 aa81a52bc9..194223053e 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 @@ -74,6 +74,6 @@ public interface UserSecretsManager { @Nonnull default String buildUrn( @Nonnull String secretManagerType, @Nonnull String typeSpecificIdentifier) { - return UserSecretReferenceUrnHelper.buildUrn(secretManagerType, typeSpecificIdentifier); + return UserSecretReference.buildUrnString(secretManagerType, typeSpecificIdentifier); } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.java similarity index 73% rename from polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java rename to polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.java index 1bdc3b045d..e05bd947bb 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceUrnHelperTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/secrets/UserSecretReferenceTest.java @@ -19,12 +19,13 @@ 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 UserSecretReferenceUrnHelperTest { +public class UserSecretReferenceTest { @ParameterizedTest @ValueSource( @@ -35,7 +36,7 @@ public class UserSecretReferenceUrnHelperTest { "urn:polaris-secret:vault:project:env:service:key" }) public void testValidUrns(String validUrn) { - assertThat(UserSecretReferenceUrnHelper.isValid(validUrn)).isTrue(); + assertThat(new UserSecretReference(validUrn, null)).isNotNull(); } @ParameterizedTest @@ -53,30 +54,30 @@ public void testValidUrns(String validUrn) { "urn:polaris-secret:unsafe-in-memory:key::" }) public void testInvalidUrns(String invalidUrn) { - assertThat(UserSecretReferenceUrnHelper.isValid(invalidUrn)) - .as("URN should be invalid: %s", invalidUrn) - .isFalse(); + 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"; - assertThat(UserSecretReferenceUrnHelper.getSecretManagerType(urn)) - .isEqualTo("unsafe-in-memory"); - assertThat(UserSecretReferenceUrnHelper.getTypeSpecificIdentifier(urn)) - .isEqualTo("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 = UserSecretReferenceUrnHelper.buildUrn("aws-secrets", "my-key"); + String urn = UserSecretReference.buildUrnString("aws-secrets", "my-key"); assertThat(urn).isEqualTo("urn:polaris-secret:aws-secrets:my-key"); String urnWithMultipleIdentifiers = - UserSecretReferenceUrnHelper.buildUrn("vault", "project:service"); + UserSecretReference.buildUrnString("vault", "project:service"); assertThat(urnWithMultipleIdentifiers).isEqualTo("urn:polaris-secret:vault:project:service"); - String urnWithNumbers = UserSecretReferenceUrnHelper.buildUrn("type_123", "456:789"); + String urnWithNumbers = UserSecretReference.buildUrnString("type_123", "456:789"); assertThat(urnWithNumbers).isEqualTo("urn:polaris-secret:type_123:456:789"); } }