This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 1773ede613 [#12218] feat(secret): Add GravitinoSecretProvider SPI and
in-memory provider (#12215)
1773ede613 is described below
commit 1773ede613d0115e955274737f1678749da0b05f
Author: MaSai <[email protected]>
AuthorDate: Mon Aug 3 19:52:56 2026 +0800
[#12218] feat(secret): Add GravitinoSecretProvider SPI and in-memory
provider (#12215)
### What changes were proposed in this pull request?
Add the secret-provider SPI (`GravitinoSecretProvider`) with:
- `initialize` / `type` / `writeSecret` / `readSecret` / `deleteSecret`
/ `close`
- a process-local `InMemorySecretsProvider` for development and unit
tests
- minimal supporting types used by the SPI (`SecretWriteContext`,
`SecretUrn`,
`SecretConstants`)
This PR intentionally does **not** wire create/alter/drop flows, REST
APIs,
provider registry configuration, or external secret references.
### Why are the changes needed?
To land the OSS foundation for entity connection-secret backends. Later
PRs can
add provider registry, create-time write-through, resolve/omit-on-read,
and REST.
Fix: #12218
### Does this PR introduce _any_ user-facing change?
- No user-facing REST/config change in this PR.
- Adds internal SPI types under `org.apache.gravitino.secret`.
### How was this patch tested?
- `./gradlew :core:test --tests
'org.apache.gravitino.secret.TestInMemorySecretsProvider' --tests
'org.apache.gravitino.secret.TestSecretUrn' -PskipITs`
---------
Co-authored-by: Cursor <[email protected]>
---
.../apache/gravitino/secret/SecretConstants.java | 41 +++++
.../org/apache/gravitino/secret/SecretUrn.java | 174 +++++++++++++++++++++
.../org/apache/gravitino/secret/TestSecretUrn.java | 77 +++++++++
.../apache/gravitino/secret/SecretProvider.java | 84 ++++++++++
.../secret/memory/InMemorySecretsProvider.java | 98 ++++++++++++
.../secret/TestInMemorySecretsProvider.java | 91 +++++++++++
6 files changed, 565 insertions(+)
diff --git a/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java
b/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java
new file mode 100644
index 0000000000..00a252c876
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SecretConstants.java
@@ -0,0 +1,41 @@
+/*
+ * 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.gravitino.secret;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Constants for Gravitino entity secret management. */
+@DeveloperApi
+public final class SecretConstants {
+
+ /** Prefix for Gravitino secret URNs. */
+ public static final String URN_PREFIX = "urn:gravitino-secret:";
+
+ /** Write attribute: entity type ({@code catalog}, {@code schema}, or {@code
fileset}). */
+ public static final String ATTR_ENTITY_TYPE = "entityType";
+
+ /** Write attribute: stable numeric entity id. */
+ public static final String ATTR_ENTITY_ID = "entityId";
+
+ /** Write attribute: entity property key that holds the secret. */
+ public static final String ATTR_PROPERTY_KEY = "propertyKey";
+
+ private SecretConstants() {}
+}
diff --git a/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java
b/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java
new file mode 100644
index 0000000000..d9bf7dc6c1
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/secret/SecretUrn.java
@@ -0,0 +1,174 @@
+/*
+ * 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.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+import static org.apache.gravitino.secret.SecretConstants.URN_PREFIX;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/**
+ * Value object for a Gravitino secret URN of the form {@code
+ * urn:gravitino-secret:<providerName>:<identifier-segments...>}.
+ */
+@DeveloperApi
+public final class SecretUrn {
+
+ // Allow '.' so dotted property keys (e.g. authentication.password) can
appear in URNs.
+ private static final Pattern SEGMENT_PATTERN =
Pattern.compile("[a-zA-Z0-9._-]+");
+
+ private final String providerName;
+ private final List<String> identifierSegments;
+
+ private SecretUrn(String providerName, List<String> identifierSegments) {
+ this.providerName = providerName;
+ this.identifierSegments = identifierSegments;
+ }
+
+ /**
+ * Returns the provider name from the URN.
+ *
+ * @return the provider name
+ */
+ public String providerName() {
+ return providerName;
+ }
+
+ /**
+ * Returns the type-specific identifier split into colon-separated segments.
+ *
+ * @return the identifier segments
+ */
+ public List<String> identifierSegments() {
+ return identifierSegments;
+ }
+
+ /**
+ * Builds a write-through secret URN for an entity property secret.
+ *
+ * <p>Required attributes: {@link SecretConstants#ATTR_ENTITY_TYPE}, {@link
+ * SecretConstants#ATTR_ENTITY_ID}, and {@link
SecretConstants#ATTR_PROPERTY_KEY}.
+ *
+ * @param providerName the configured provider name
+ * @param attributes write-through attributes
+ * @return the write-through secret URN
+ */
+ public static SecretUrn buildWriteThrough(String providerName, Map<String,
String> attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("attributes must not be null");
+ }
+ String entityType = requiredAttribute(attributes, ATTR_ENTITY_TYPE);
+ String entityId = requiredAttribute(attributes, ATTR_ENTITY_ID);
+ String propertyKey = requiredAttribute(attributes, ATTR_PROPERTY_KEY);
+ try {
+ Long.parseLong(entityId);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "attributes." + ATTR_ENTITY_ID + " must be a numeric entity id: " +
entityId, e);
+ }
+ validateSegment(providerName);
+ validateSegment(entityType);
+ validateSegment(entityId);
+ validateSegment(propertyKey);
+ return new SecretUrn(
+ providerName,
+ Collections.unmodifiableList(Arrays.asList(entityType, entityId,
propertyKey)));
+ }
+
+ /**
+ * Parses a Gravitino secret URN string.
+ *
+ * @param urn the secret URN string
+ * @return the parsed secret URN
+ */
+ public static SecretUrn parse(String urn) {
+ if (!StringUtils.startsWith(urn, URN_PREFIX)) {
+ throw new IllegalArgumentException("Invalid Gravitino secret URN: " +
urn);
+ }
+
+ String remainder = urn.substring(URN_PREFIX.length());
+ if (StringUtils.isEmpty(remainder)) {
+ throw new IllegalArgumentException("Invalid Gravitino secret URN: " +
urn);
+ }
+
+ String[] segments = remainder.split(":", -1);
+ if (segments.length < 2) {
+ throw new IllegalArgumentException("Invalid Gravitino secret URN: " +
urn);
+ }
+
+ for (String segment : segments) {
+ validateSegment(segment);
+ }
+
+ String[] identifierParts = Arrays.copyOfRange(segments, 1,
segments.length);
+ return new SecretUrn(segments[0],
Collections.unmodifiableList(Arrays.asList(identifierParts)));
+ }
+
+ /**
+ * Validates that a URN segment contains only allowed characters.
+ *
+ * @param segment the segment to validate
+ */
+ public static void validateSegment(String segment) {
+ if (StringUtils.isEmpty(segment) ||
!SEGMENT_PATTERN.matcher(segment).matches()) {
+ throw new IllegalArgumentException("Invalid URN segment: " + segment);
+ }
+ }
+
+ private static String requiredAttribute(Map<String, String> attributes,
String key) {
+ String value = attributes.get(key);
+ if (StringUtils.isBlank(value)) {
+ throw new IllegalArgumentException("attributes." + key + " must not be
blank");
+ }
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return URN_PREFIX + providerName + ":" + String.join(":",
identifierSegments);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SecretUrn)) {
+ return false;
+ }
+ SecretUrn that = (SecretUrn) other;
+ return Objects.equals(providerName, that.providerName)
+ && Objects.equals(identifierSegments, that.identifierSegments);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(providerName, identifierSegments);
+ }
+}
diff --git a/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java
b/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java
new file mode 100644
index 0000000000..be0c42f3e6
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/secret/TestSecretUrn.java
@@ -0,0 +1,77 @@
+/*
+ * 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.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSecretUrn {
+
+ private static Map<String, String> attributes(String entityType, String
entityId, String key) {
+ return Map.of(
+ ATTR_ENTITY_TYPE, entityType,
+ ATTR_ENTITY_ID, entityId,
+ ATTR_PROPERTY_KEY, key);
+ }
+
+ @Test
+ public void testBuildAndParseWriteThroughUrn() {
+ SecretUrn urn =
+ SecretUrn.buildWriteThrough("memory", attributes("catalog", "42",
"jdbc-password"));
+
Assertions.assertEquals("urn:gravitino-secret:memory:catalog:42:jdbc-password",
urn.toString());
+ Assertions.assertEquals("memory", urn.providerName());
+ Assertions.assertEquals(List.of("catalog", "42", "jdbc-password"),
urn.identifierSegments());
+
+ SecretUrn parsed = SecretUrn.parse(urn.toString());
+ Assertions.assertEquals(urn, parsed);
+ }
+
+ @Test
+ public void testDottedPropertyKeyInUrn() {
+ SecretUrn urn =
+ SecretUrn.buildWriteThrough("local", attributes("catalog", "1",
"authentication.password"));
+ Assertions.assertEquals(
+ "urn:gravitino-secret:local:catalog:1:authentication.password",
urn.toString());
+ Assertions.assertEquals("local", urn.providerName());
+ Assertions.assertEquals(
+ List.of("catalog", "1", "authentication.password"),
urn.identifierSegments());
+ }
+
+ @Test
+ public void testInvalidUrn() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
SecretUrn.parse("not-a-urn"));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> SecretUrn.buildWriteThrough("memory", attributes("catalog", "1",
"bad key")));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> SecretUrn.buildWriteThrough("memory", attributes("catalog",
"abc", "password")));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
SecretUrn.buildWriteThrough("memory", null));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
SecretUrn.validateSegment("bad/key"));
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java
b/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java
new file mode 100644
index 0000000000..2db4d8421f
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/secret/SecretProvider.java
@@ -0,0 +1,84 @@
+/*
+ * 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.gravitino.secret;
+
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Service provider interface for secret backends. */
+@DeveloperApi
+public interface SecretProvider {
+
+ /**
+ * Initializes this provider after construction.
+ *
+ * <p>Implementations must override this method and explicitly decide
whether configuration is
+ * required. An empty body is acceptable for providers that need no setup.
+ *
+ * @param name the configured provider instance name
+ * @param config provider-specific configuration (without the {@code
gravitino.secret.provider.
+ * <name>.} prefix)
+ */
+ void initialize(String name, Map<String, String> config);
+
+ /**
+ * Returns the provider type identifier.
+ *
+ * @return the provider type
+ */
+ String type();
+
+ /**
+ * Writes a plaintext secret and returns its URN.
+ *
+ * <p>Provider-specific write metadata is supplied as {@code attributes}.
Required keys depend on
+ * the provider implementation; for example the in-memory write-through
provider expects {@link
+ * SecretConstants#ATTR_ENTITY_TYPE}, {@link
SecretConstants#ATTR_ENTITY_ID}, and {@link
+ * SecretConstants#ATTR_PROPERTY_KEY}.
+ *
+ * @param plaintext the secret plaintext
+ * @param attributes provider-specific write attributes
+ * @return the secret URN
+ */
+ SecretUrn writeSecret(String plaintext, Map<String, String> attributes);
+
+ /**
+ * Reads a secret by URN.
+ *
+ * @param urn the secret URN
+ * @return the secret plaintext
+ */
+ String readSecret(SecretUrn urn);
+
+ /**
+ * Deletes a secret by URN.
+ *
+ * @param urn the secret URN
+ */
+ void deleteSecret(SecretUrn urn);
+
+ /**
+ * Releases resources owned by this provider.
+ *
+ * <p>Implementations must override this method and explicitly release any
held resources. An
+ * empty body is acceptable when there is nothing to clean up.
+ */
+ void close();
+}
diff --git
a/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
b/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
new file mode 100644
index 0000000000..3df5166980
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/secret/memory/InMemorySecretsProvider.java
@@ -0,0 +1,98 @@
+/*
+ * 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.gravitino.secret.memory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.secret.SecretProvider;
+import org.apache.gravitino.secret.SecretUrn;
+
+/**
+ * In-memory secret provider for development and unit tests only.
+ *
+ * <p>Secrets are stored in process memory only and are lost on restart.
Values are Base64-encoded
+ * for opaque storage, which is <strong>not</strong> encryption. {@link
#close()} clears map
+ * references but does not securely zero heap contents ({@link String} is not
wipeable); that is
+ * acceptable only because this backend is test-oriented. Production providers
should store wipeable
+ * {@code byte[]}/{@code char[]} and explicitly zero them on delete/close. Do
not use this provider
+ * in production.
+ */
+public class InMemorySecretsProvider implements SecretProvider {
+
+ private final ConcurrentHashMap<String, String> secrets = new
ConcurrentHashMap<>();
+ private String providerName;
+
+ @Override
+ public void initialize(String name, Map<String, String> config) {
+ if (StringUtils.isBlank(name)) {
+ throw new IllegalArgumentException("provider name must not be blank");
+ }
+ this.providerName = name;
+ }
+
+ @Override
+ public String type() {
+ return "memory";
+ }
+
+ @Override
+ public SecretUrn writeSecret(String plaintext, Map<String, String>
attributes) {
+ if (plaintext == null) {
+ throw new IllegalArgumentException("plaintext must not be null");
+ }
+ if (providerName == null) {
+ throw new IllegalStateException("InMemorySecretsProvider is not
initialized");
+ }
+
+ SecretUrn urn = SecretUrn.buildWriteThrough(providerName, attributes);
+ secrets.put(
+ urn.toString(),
+
Base64.getEncoder().encodeToString(plaintext.getBytes(StandardCharsets.UTF_8)));
+ return urn;
+ }
+
+ @Override
+ public String readSecret(SecretUrn urn) {
+ if (urn == null) {
+ throw new IllegalArgumentException("urn must not be null");
+ }
+ String encoded = secrets.get(urn.toString());
+ if (encoded == null) {
+ throw new IllegalArgumentException("Secret not found for URN: " + urn);
+ }
+ return new String(Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public void deleteSecret(SecretUrn urn) {
+ if (urn == null) {
+ throw new IllegalArgumentException("urn must not be null");
+ }
+ secrets.remove(urn.toString());
+ }
+
+ @Override
+ public void close() {
+ secrets.clear();
+ }
+}
diff --git
a/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
b/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
new file mode 100644
index 0000000000..33d9072db4
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/secret/TestInMemorySecretsProvider.java
@@ -0,0 +1,91 @@
+/*
+ * 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.gravitino.secret;
+
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_ID;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_ENTITY_TYPE;
+import static org.apache.gravitino.secret.SecretConstants.ATTR_PROPERTY_KEY;
+
+import java.util.Map;
+import org.apache.gravitino.secret.memory.InMemorySecretsProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestInMemorySecretsProvider {
+
+ private static Map<String, String> writeAttributes() {
+ return Map.of(
+ ATTR_ENTITY_TYPE, "catalog",
+ ATTR_ENTITY_ID, "10",
+ ATTR_PROPERTY_KEY, "password");
+ }
+
+ @Test
+ public void testWriteReadDelete() {
+ InMemorySecretsProvider provider = new InMemorySecretsProvider();
+ Assertions.assertEquals("memory", provider.type());
+
+ provider.initialize("memory", Map.of());
+ SecretUrn urn = provider.writeSecret("s3cr3t", writeAttributes());
+ Assertions.assertEquals("urn:gravitino-secret:memory:catalog:10:password",
urn.toString());
+ Assertions.assertEquals("s3cr3t", provider.readSecret(urn));
+
+ provider.deleteSecret(urn);
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
provider.readSecret(urn));
+ provider.close();
+ }
+
+ @Test
+ public void testWriteSecretRejectsNullArguments() {
+ InMemorySecretsProvider provider = new InMemorySecretsProvider();
+ provider.initialize("memory", Map.of());
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> provider.writeSecret(null,
writeAttributes()));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> provider.writeSecret("s3cr3t",
null));
+ }
+
+ @Test
+ public void testWriteSecretRejectsMissingAttributes() {
+ InMemorySecretsProvider provider = new InMemorySecretsProvider();
+ provider.initialize("memory", Map.of());
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> provider.writeSecret("s3cr3t",
Map.of()));
+ }
+
+ @Test
+ public void testReadDeleteRejectNullUrn() {
+ InMemorySecretsProvider provider = new InMemorySecretsProvider();
+ provider.initialize("memory", Map.of());
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
provider.readSecret(null));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
provider.deleteSecret(null));
+ }
+
+ @Test
+ public void testCloseClearsStoredSecrets() {
+ InMemorySecretsProvider provider = new InMemorySecretsProvider();
+ provider.initialize("memory", Map.of());
+ SecretUrn urn = provider.writeSecret("s3cr3t", writeAttributes());
+ Assertions.assertEquals("s3cr3t", provider.readSecret(urn));
+
+ provider.close();
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
provider.readSecret(urn));
+ }
+}