Skip to content

Commit 45df8ac

Browse files
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.
1 parent 48e7e88 commit 45df8ac

File tree

4 files changed

+213
-16
lines changed

4 files changed

+213
-16
lines changed

polaris-core/src/main/java/org/apache/polaris/core/secrets/UnsafeInMemorySecretsManager.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.polaris.core.secrets;
2020

21+
import com.google.common.base.Preconditions;
2122
import jakarta.annotation.Nonnull;
2223
import java.nio.charset.StandardCharsets;
2324
import java.security.SecureRandom;
@@ -34,9 +35,6 @@
3435
* development purposes.
3536
*/
3637
public class UnsafeInMemorySecretsManager implements UserSecretsManager {
37-
// TODO: Remove this and wire into QuarkusProducers; just a placeholder for now to get the
38-
// rest of the logic working.
39-
public static final UserSecretsManager GLOBAL_INSTANCE = new UnsafeInMemorySecretsManager();
4038

4139
private final Map<String, String> rawSecretStore = new ConcurrentHashMap<>();
4240
private final SecureRandom rand = new SecureRandom();
@@ -45,6 +43,8 @@ public class UnsafeInMemorySecretsManager implements UserSecretsManager {
4543
private static final String CIPHERTEXT_HASH = "ciphertext-hash";
4644
private static final String ENCRYPTION_KEY = "encryption-key";
4745

46+
public static final String SECRET_MANAGER_TYPE = "unsafe-in-memory";
47+
4848
/** {@inheritDoc} */
4949
@Override
5050
@Nonnull
@@ -73,10 +73,8 @@ public UserSecretReference writeSecret(
7373

7474
String secretUrn;
7575
for (int secretOrdinal = 0; ; ++secretOrdinal) {
76-
secretUrn =
77-
String.format(
78-
"urn:polaris-secret:unsafe-in-memory:%d:%d", forEntity.getId(), secretOrdinal);
79-
76+
String typeSpecificIdentifier = forEntity.getId() + ":" + secretOrdinal;
77+
secretUrn = buildUrn(SECRET_MANAGER_TYPE, typeSpecificIdentifier);
8078
// Store the base64-encoded encrypted ciphertext in the simulated "secret store".
8179
String existingSecret =
8280
rawSecretStore.putIfAbsent(secretUrn, encryptedSecretCipherTextBase64);
@@ -107,7 +105,14 @@ public UserSecretReference writeSecret(
107105
@Override
108106
@Nonnull
109107
public String readSecret(@Nonnull UserSecretReference secretReference) {
110-
// TODO: Precondition checks and/or wire in PolarisDiagnostics
108+
String secretManagerType = secretReference.getUserSecretManagerType();
109+
Preconditions.checkState(
110+
secretManagerType.equals(SECRET_MANAGER_TYPE),
111+
"Invalid secret manager type, expected: "
112+
+ SECRET_MANAGER_TYPE
113+
+ " got: "
114+
+ secretManagerType);
115+
111116
String encryptedSecretCipherTextBase64 = rawSecretStore.get(secretReference.getUrn());
112117
if (encryptedSecretCipherTextBase64 == null) {
113118
// Secret at this URN no longer exists.

polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretReference.java

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.util.HashMap;
2828
import java.util.Map;
2929
import java.util.Objects;
30+
import java.util.regex.Matcher;
31+
import java.util.regex.Pattern;
3032

3133
/**
3234
* Represents a "wrapped reference" to a user-owned secret that holds an identifier to retrieve
@@ -56,6 +58,43 @@ public class UserSecretReference {
5658
@JsonProperty(value = "referencePayload")
5759
private final Map<String, String> referencePayload;
5860

61+
private static final String URN_SCHEME = "urn";
62+
private static final String URN_NAMESPACE = "polaris-secret";
63+
private static final String SECRET_MANAGER_TYPE_REGEX = "([a-zA-Z0-9_-]+)";
64+
private static final String TYPE_SPECIFIC_IDENTIFIER_REGEX =
65+
"([a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*)";
66+
67+
/**
68+
* Precompiled regex pattern for validating the secret manager type and type-specific identifier.
69+
*/
70+
private static final Pattern SECRET_MANAGER_TYPE_PATTERN =
71+
Pattern.compile("^" + SECRET_MANAGER_TYPE_REGEX + "$");
72+
73+
private static final Pattern TYPE_SPECIFIC_IDENTIFIER_PATTERN =
74+
Pattern.compile("^" + TYPE_SPECIFIC_IDENTIFIER_REGEX + "$");
75+
76+
/**
77+
* Precompiled regex pattern for validating and parsing UserSecretReference URNs. Expected format:
78+
* urn:polaris-secret:<secret-manager-type>:<identifier1>(:<identifier2>:...).
79+
*
80+
* <p>Groups:
81+
*
82+
* <p>Group 1: secret-manager-type (alphanumeric, hyphens, underscores).
83+
*
84+
* <p>Group 2: type-specific-identifier (one or more colon-separated alphanumeric components).
85+
*/
86+
private static final Pattern URN_PATTERN =
87+
Pattern.compile(
88+
"^"
89+
+ URN_SCHEME
90+
+ ":"
91+
+ URN_NAMESPACE
92+
+ ":"
93+
+ SECRET_MANAGER_TYPE_REGEX
94+
+ ":"
95+
+ TYPE_SPECIFIC_IDENTIFIER_REGEX
96+
+ "$");
97+
5998
/**
6099
* @param urn A string which should be self-sufficient to retrieve whatever secret material that
61100
* is stored in the remote secret store and also to identify an implementation of the
@@ -70,26 +109,83 @@ public class UserSecretReference {
70109
public UserSecretReference(
71110
@JsonProperty(value = "urn", required = true) @Nonnull String urn,
72111
@JsonProperty(value = "referencePayload") @Nullable Map<String, String> referencePayload) {
73-
// TODO: Add better/standardized parsing and validation of URN syntax
74112
Preconditions.checkArgument(
75-
urn.startsWith("urn:polaris-secret:") && urn.split(":").length >= 4,
76-
"Invalid secret URN '%s'; must be of the form "
77-
+ "'urn:polaris-secret:<secret-manager-type>:<type-specific-identifier>'",
78-
urn);
113+
urnIsValid(urn),
114+
"Invalid secret URN: " + urn + "; must be of the form: " + URN_PATTERN.toString());
79115
this.urn = urn;
80116
this.referencePayload = Objects.requireNonNullElse(referencePayload, new HashMap<>());
81117
}
82118

119+
/**
120+
* Validates whether the given URN string matches the expected format for UserSecretReference
121+
* URNs.
122+
*
123+
* @param urn The URN string to validate.
124+
* @return true if the URN is valid, false otherwise.
125+
*/
126+
private static boolean urnIsValid(@Nonnull String urn) {
127+
return urn.trim().isEmpty() ? false : URN_PATTERN.matcher(urn).matches();
128+
}
129+
130+
/**
131+
* Builds a URN string from the given secret manager type and type-specific identifier. Validates
132+
* the inputs to ensure they conform to the expected pattern.
133+
*
134+
* @param secretManagerType The secret manager type (alphanumeric, hyphens, underscores).
135+
* @param typeSpecificIdentifier The type-specific identifier (colon-separated alphanumeric
136+
* components).
137+
* @return The constructed URN string.
138+
*/
139+
@Nonnull
140+
public static String buildUrnString(
141+
@Nonnull String secretManagerType, @Nonnull String typeSpecificIdentifier) {
142+
143+
Preconditions.checkArgument(
144+
!secretManagerType.trim().isEmpty(), "Secret manager type cannot be empty");
145+
Preconditions.checkArgument(
146+
SECRET_MANAGER_TYPE_PATTERN.matcher(secretManagerType).matches(),
147+
"Invalid secret manager type '%s'; must contain only alphanumeric characters, hyphens, and underscores",
148+
secretManagerType);
149+
150+
Preconditions.checkArgument(
151+
!typeSpecificIdentifier.trim().isEmpty(), "Type-specific identifier cannot be empty");
152+
Preconditions.checkArgument(
153+
TYPE_SPECIFIC_IDENTIFIER_PATTERN.matcher(typeSpecificIdentifier).matches(),
154+
"Invalid type-specific identifier '%s'; must be colon-separated alphanumeric components (hyphens and underscores allowed)",
155+
typeSpecificIdentifier);
156+
157+
return URN_SCHEME
158+
+ ":"
159+
+ URN_NAMESPACE
160+
+ ":"
161+
+ secretManagerType
162+
+ ":"
163+
+ typeSpecificIdentifier;
164+
}
165+
83166
/**
84167
* Since UserSecretReference objects are specific to UserSecretManager implementations, the
85168
* "secret-manager-type" portion of the URN should be used to validate that a URN is valid for a
86169
* given implementation and to dispatch to the correct implementation at runtime if multiple
87170
* concurrent implementations are possible in a given runtime environment.
88171
*/
89172
@JsonIgnore
90-
public String getUserSecretManagerTypeFromUrn() {
91-
// TODO: Add better/standardized parsing and validation of URN syntax
92-
return urn.split(":")[2];
173+
public String getUserSecretManagerType() {
174+
Matcher matcher = URN_PATTERN.matcher(urn);
175+
Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn);
176+
return matcher.group(1);
177+
}
178+
179+
/**
180+
* Returns the type-specific identifier from the URN. Since the format is specific to the
181+
* UserSecretManager implementation, this method does not validate the identifier. It is the
182+
* responsibility of the caller to validate it.
183+
*/
184+
@JsonIgnore
185+
public String getTypeSpecificIdentifier() {
186+
Matcher matcher = URN_PATTERN.matcher(urn);
187+
Preconditions.checkState(matcher.matches(), "Invalid secret URN: " + urn);
188+
return matcher.group(2);
93189
}
94190

95191
public @Nonnull String getUrn() {

polaris-core/src/main/java/org/apache/polaris/core/secrets/UserSecretsManager.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,17 @@ public interface UserSecretsManager {
6363
* @param secretReference Reference object for retrieving the original secret
6464
*/
6565
void deleteSecret(@Nonnull UserSecretReference secretReference);
66+
67+
/**
68+
* Builds a URN string from the given secret manager type and type-specific identifier.
69+
*
70+
* @param typeSpecificIdentifier The type-specific identifier (colon-separated alphanumeric
71+
* components with underscores and hyphens).
72+
* @return The constructed URN string.
73+
*/
74+
@Nonnull
75+
default String buildUrn(
76+
@Nonnull String secretManagerType, @Nonnull String typeSpecificIdentifier) {
77+
return UserSecretReference.buildUrnString(secretManagerType, typeSpecificIdentifier);
78+
}
6679
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.secrets;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.ValueSource;
27+
28+
public class UserSecretReferenceTest {
29+
30+
@ParameterizedTest
31+
@ValueSource(
32+
strings = {
33+
"urn:polaris-secret:unsafe-in-memory:key1",
34+
"urn:polaris-secret:unsafe-in-memory:key1:value1",
35+
"urn:polaris-secret:aws_secrets-manager:my-key_123",
36+
"urn:polaris-secret:vault:project:env:service:key"
37+
})
38+
public void testValidUrns(String validUrn) {
39+
assertThat(new UserSecretReference(validUrn, null)).isNotNull();
40+
}
41+
42+
@ParameterizedTest
43+
@ValueSource(
44+
strings = {
45+
"",
46+
" ",
47+
"not-a-urn",
48+
"urn:",
49+
"urn:polaris-secret:",
50+
"urn:polaris-secret:type:",
51+
"wrong:polaris-secret:type:key:",
52+
"urn:polaris-secret:type with spaces:key",
53+
"urn:polaris-secret:type@invalid:key",
54+
"urn:polaris-secret:unsafe-in-memory:key::"
55+
})
56+
public void testInvalidUrns(String invalidUrn) {
57+
assertThatThrownBy(() -> new UserSecretReference(invalidUrn, null))
58+
.isInstanceOf(IllegalArgumentException.class)
59+
.hasMessageContaining("Invalid secret URN: " + invalidUrn);
60+
}
61+
62+
@Test
63+
public void tesGetUrnComponents() {
64+
String urn = "urn:polaris-secret:unsafe-in-memory:key1:value1";
65+
UserSecretReference reference = new UserSecretReference(urn, null);
66+
67+
assertThat(reference.getUserSecretManagerType()).isEqualTo("unsafe-in-memory");
68+
assertThat(reference.getTypeSpecificIdentifier()).isEqualTo("key1:value1");
69+
}
70+
71+
@Test
72+
public void testBuildUrn() {
73+
String urn = UserSecretReference.buildUrnString("aws-secrets", "my-key");
74+
assertThat(urn).isEqualTo("urn:polaris-secret:aws-secrets:my-key");
75+
76+
String urnWithMultipleIdentifiers =
77+
UserSecretReference.buildUrnString("vault", "project:service");
78+
assertThat(urnWithMultipleIdentifiers).isEqualTo("urn:polaris-secret:vault:project:service");
79+
80+
String urnWithNumbers = UserSecretReference.buildUrnString("type_123", "456:789");
81+
assertThat(urnWithNumbers).isEqualTo("urn:polaris-secret:type_123:456:789");
82+
}
83+
}

0 commit comments

Comments
 (0)