properties;
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java
index 92969d0ff..59e570a95 100644
--- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java
@@ -10,7 +10,7 @@
/**
* Represent a secret in the Secret Manager. It is a map key/secret.
- * It can have an expiration to help cache eviction.
+ * It can have an pollInterval to help cache eviction.
*
* Secrets can be pulled directly or using {@link WellKnownSecretKey} for well known secrets type (TLS and Basic Auth).
* An explicit call to {@link #handleWellKnownSecretKeys(Map)} with a mapping must be performed to extract well-known keys.
@@ -26,7 +26,7 @@ public final class SecretMap {
private final Instant expireAt;
/**
- * Create a {@link SecretMap} from a map of {@link Secret} without expiration
+ * Create a {@link SecretMap} from a map of {@link Secret} without pollInterval
*
* @param map the map of {@link Secret}
*/
@@ -35,16 +35,24 @@ public SecretMap(Map map) {
}
/**
- * Create a {@link SecretMap} from a map of {@link Secret} with expiration
+ * Create a {@link SecretMap} from a map of {@link Secret} with pollInterval
*
* @param map the map of {@link Secret}
- * @param expireAt expiration
+ * @param expireAt pollInterval
*/
public SecretMap(Map map, Instant expireAt) {
this.map = map == null ? Map.of() : Map.copyOf(map);
this.expireAt = expireAt;
}
+ /**
+ *
+ * @return a copy f the secrets
+ */
+ public Map asMap() {
+ return Map.copyOf(map);
+ }
+
/**
* Builds a secret map where secrets are base64 encoded
*
@@ -57,7 +65,7 @@ public static SecretMap ofBase64(Map data) {
}
/**
- * Builds a secret map where secrets are base64 encoded with expiration date
+ * Builds a secret map where secrets are base64 encoded with pollInterval date
*
* @param data the secret as a map (String/byte[] or String/String) where bytes or String are base64 encoded
* @param expireAt when the secret expires
@@ -80,7 +88,7 @@ public static SecretMap of(Map data) {
}
/**
- * Builds a secret map with expiration date
+ * Builds a secret map with pollInterval date
*
* @param data the secret as a map (String/byte[] or String/String)
* @param expireAt when the secret expires
@@ -106,7 +114,7 @@ public Optional getSecret(SecretMount secretMount) {
}
/**
- * @return optional of the expiration of this secret
+ * @return optional of the pollInterval of this secret
*/
public Optional expireAt() {
return Optional.ofNullable(expireAt);
@@ -141,9 +149,13 @@ public Optional wellKnown(WellKnownSecretKey key) {
return Optional.ofNullable(wellKnown.get(key));
}
+ public SecretMap withExpireAt(Instant expireAt) {
+ return new SecretMap(map, expireAt);
+ }
+
/**
* Well-known field that can typically exist find in a secret. This is from Gravitee.io point of view.
- * Any consumer of those field should use {@link SecretMap#wellKnown()} to use fetch the data.
+ * Any consumer of those field should use {@link SecretMap#wellKnown(WellKnownSecretKey)} to use fetch the data.
*/
public enum WellKnownSecretKey {
CERTIFICATE,
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMount.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMount.java
index 517f71311..ea33014d5 100644
--- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMount.java
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMount.java
@@ -10,7 +10,7 @@
* @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
* @author GraviteeSource Team
*/
-public record SecretMount(String provider, SecretLocation location, String key, SecretURL secretURL) {
+public record SecretMount(String provider, SecretLocation location, String key, SecretURL secretURL, boolean retryOnError) {
/**
* Test the presence of a key
*
@@ -19,4 +19,8 @@ public record SecretMount(String provider, SecretLocation location, String key,
public boolean isKeyEmpty() {
return Strings.isNullOrEmpty(key);
}
+
+ public SecretMount withoutRetries() {
+ return new SecretMount(provider, location, key, secretURL, false);
+ }
}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretURL.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretURL.java
index ee310987e..cee788735 100644
--- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretURL.java
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretURL.java
@@ -3,7 +3,10 @@
import com.google.common.base.Splitter;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
-import java.util.*;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@@ -14,7 +17,7 @@
* @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
* @author GraviteeSource Team
*/
-public record SecretURL(String provider, String path, String key, Multimap query) {
+public record SecretURL(String provider, String path, String key, Multimap query, boolean pluginIdMatchURLProvider) {
public static final char URL_SEPARATOR = '/';
private static final Splitter urlPathSplitter = Splitter.on(URL_SEPARATOR);
private static final Splitter queryParamSplitter = Splitter.on('&');
@@ -22,12 +25,16 @@ public record SecretURL(String provider, String path, String key, Multimap
* the format is : secret://<provider>/<path or name>[:<key>][?option=value1&option=value2]
*
- * secret://is mandatory
+ * secret://
is mandatory is includesSchema is true
* provider is mandatory and should match a secret provider id
* "path or name" is mandatory, a free string that can contain forward slashes ('/').
* If an empty string or spaces are found between two forward slashes (eg. //
or / /
) parsing will fail.
@@ -35,16 +42,17 @@ public record SecretURL(String provider, String path, String key, Multimapquery string is optional and is simply split into key/value pairs.
* Pair are always list as can be specified more than once. If no value is parsed, then true
is set
*
- * @param url the string to parse
+ * @param url the string to parse
+ * @param includesSchema to indicate
* @return SecretURL object
* @throws IllegalArgumentException when failing to parse
*/
- public static SecretURL from(String url) {
+ public static SecretURL from(String url, boolean includesSchema) {
url = Objects.requireNonNull(url).trim();
- if (!url.startsWith(SCHEME)) {
+ if (includesSchema && !url.startsWith(SCHEME)) {
throwFormatError(url);
}
- String schemeLess = url.substring(SCHEME.length());
+ String schemeLess = includesSchema ? url.substring(SCHEME.length()) : url.substring(1);
int firstSlash = schemeLess.indexOf('/');
if (firstSlash < 0 || firstSlash == schemeLess.length() - 1) {
throwFormatError(url);
@@ -89,7 +97,7 @@ public static SecretURL from(String url) {
throwFormatError(url);
}
- return new SecretURL(provider, path, key, query);
+ return new SecretURL(provider, path, key, query, includesSchema);
}
private static void throwFormatError(String url) {
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/RuntimeSecretException.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/RuntimeSecretException.java
new file mode 100644
index 000000000..3f32f7879
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/RuntimeSecretException.java
@@ -0,0 +1,22 @@
+package io.gravitee.node.api.secrets.runtime;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class RuntimeSecretException extends RuntimeException {
+
+ public RuntimeSecretException() {}
+
+ public RuntimeSecretException(String message) {
+ super(message);
+ }
+
+ public RuntimeSecretException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RuntimeSecretException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/ContextRegistry.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/ContextRegistry.java
new file mode 100644
index 000000000..5163125f4
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/ContextRegistry.java
@@ -0,0 +1,15 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.util.List;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface ContextRegistry {
+ void register(DiscoveryContext context, Definition definition);
+ List findBySpec(Spec spec);
+ List getByDefinition(String envId, Definition definition);
+ void unregister(DiscoveryContext context, Definition definition);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Definition.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Definition.java
new file mode 100644
index 000000000..4698e60ab
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Definition.java
@@ -0,0 +1,5 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import java.util.Optional;
+
+public record Definition(String kind, String id, Optional revision) {}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionBrowser.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionBrowser.java
new file mode 100644
index 000000000..44e867211
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionBrowser.java
@@ -0,0 +1,15 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface DefinitionBrowser {
+ boolean canHandle(Object definition);
+
+ Definition getDefinitionLocation(T definition, Map metadata);
+
+ void findPayloads(T definition, DefinitionPayloadNotifier notifier);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionPayloadNotifier.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionPayloadNotifier.java
new file mode 100644
index 000000000..ddf3f3ea1
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionPayloadNotifier.java
@@ -0,0 +1,11 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import java.util.function.Consumer;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface DefinitionPayloadNotifier {
+ void onPayload(String payload, PayloadLocation location, Consumer updatedPayload);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryContext.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryContext.java
new file mode 100644
index 000000000..31afeddaa
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryContext.java
@@ -0,0 +1,9 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import java.util.UUID;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record DiscoveryContext(UUID id, String envId, Ref ref, DiscoveryLocation location) {}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryLocation.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryLocation.java
new file mode 100644
index 000000000..19411609e
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryLocation.java
@@ -0,0 +1,9 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record DiscoveryLocation(Definition definition, PayloadLocation... payloadLocations) {
+ public record Definition(String kind, String id) {}
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/PayloadLocation.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/PayloadLocation.java
new file mode 100644
index 000000000..65d6f6a54
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/PayloadLocation.java
@@ -0,0 +1,6 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+public record PayloadLocation(String kind, String id) {
+ public static final String PLUGIN_KIND = "plugin";
+ public static final PayloadLocation NOWHERE = new PayloadLocation("", "");
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java
new file mode 100644
index 000000000..8c01d8af5
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java
@@ -0,0 +1,75 @@
+package io.gravitee.node.api.secrets.runtime.discovery;
+
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+public record Ref(
+ MainType mainType,
+ Expression mainExpression,
+ SecondaryType secondaryType,
+ Expression secondaryExpression,
+ String rawRef
+) {
+ public record Expression(String value, boolean isEL) {
+ public boolean isLiteral() {
+ return !isEL;
+ }
+ }
+
+ public enum MainType {
+ NAME,
+ URI,
+ }
+
+ public enum SecondaryType {
+ NAME,
+ URI,
+ KEY,
+ }
+
+ public static final String URI_KEY_SEPARATOR = ":";
+
+ public Spec asOnTheFlySpec(String envId) {
+ return new Spec(
+ null,
+ null,
+ mainExpression().value(),
+ secondaryExpression().value(),
+ null,
+ mainType() == MainType.URI && mainExpression.isLiteral() && (secondaryType() == null || secondaryExpression().isEL()),
+ true,
+ null,
+ null,
+ envId
+ );
+ }
+
+ /**
+ * Check type are compatible and call {@link #formatUriAndKey(String, String)}
+ * @return main value and secondary value with key alias separator
+ */
+ public String uriAndKey() {
+ if (mainType == MainType.URI && secondaryType == SecondaryType.KEY) {
+ return formatUriAndKey(mainExpression.value(), secondaryExpression.value());
+ }
+ throw new IllegalArgumentException(
+ "cannot format main is %s and secondary is %s it should %s and %s".formatted(
+ mainType,
+ secondaryType,
+ MainType.URI,
+ SecondaryType.KEY
+ )
+ );
+ }
+
+ /**
+ * @return main value and secondary value with key alias separator
+ */
+ public static String formatUriAndKey(String uri, String key) {
+ return uri + URI_KEY_SEPARATOR + key;
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/Grant.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/Grant.java
new file mode 100644
index 000000000..7614f76e9
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/Grant.java
@@ -0,0 +1,26 @@
+package io.gravitee.node.api.secrets.runtime.grant;
+
+import io.gravitee.common.secrets.RuntimeContext;
+import io.gravitee.common.secrets.ValueKind;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+public record Grant(CacheKey cacheKey, String secretKey, ValueKind allowedKind, Set allowedFields) {
+ public boolean match(RuntimeContext runtimeContext) {
+ if (runtimeContext == null) {
+ return Boolean.TRUE;
+ } else {
+ Predicate allowed = grant -> runtimeContext.allowed();
+ Predicate valueKindMatch = grant -> grant.allowedKind() == null || grant.allowedKind() == runtimeContext.allowKind();
+ Predicate noACLSFields = grant -> grant.allowedFields().isEmpty();
+ Predicate fieldMatch = grant -> grant.allowedFields().contains(runtimeContext.field().toLowerCase());
+ return allowed.and(valueKindMatch).and(noACLSFields.or(fieldMatch)).test(this);
+ }
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java
new file mode 100644
index 000000000..36bdfaf8c
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java
@@ -0,0 +1,17 @@
+package io.gravitee.node.api.secrets.runtime.grant;
+
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.util.Optional;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface GrantService {
+ Optional getGrant(String contextId);
+
+ boolean grant(DiscoveryContext context, Spec spec);
+
+ void revoke(DiscoveryContext context);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java
new file mode 100644
index 000000000..7dc705638
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java
@@ -0,0 +1,22 @@
+package io.gravitee.node.api.secrets.runtime.providers;
+
+import io.gravitee.node.api.secrets.model.SecretMount;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.reactivex.rxjava3.annotations.NonNull;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.functions.Action;
+import java.time.Duration;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface ResolverService {
+ Single resolve(String envId, SecretMount secretMount);
+
+ void resolveAsync(Spec spec, Duration delayBeforeResolve, boolean retryOnError, @NonNull Action postResolution);
+
+ Single toSecretMount(String envId, SecretURL secretURL);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/SecretProviderDeployer.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/SecretProviderDeployer.java
new file mode 100644
index 000000000..136f59c3a
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/SecretProviderDeployer.java
@@ -0,0 +1,13 @@
+package io.gravitee.node.api.secrets.runtime.providers;
+
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface SecretProviderDeployer {
+ default void init() {}
+
+ void deploy(String pluginId, Map config, String providerId, String envId);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ACLs.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ACLs.java
new file mode 100644
index 000000000..8aaed265e
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ACLs.java
@@ -0,0 +1,14 @@
+package io.gravitee.node.api.secrets.runtime.spec;
+
+import io.gravitee.common.secrets.ValueKind;
+import java.util.List;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+public record ACLs(ValueKind valueKind, List definitions, List plugins) {
+ public record DefinitionACL(String kind, List ids) {}
+ public record PluginACL(String id, List fields) {}
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java
new file mode 100644
index 000000000..90a8410a7
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java
@@ -0,0 +1,15 @@
+package io.gravitee.node.api.secrets.runtime.spec;
+
+import java.time.Duration;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Resolution(Type type, Duration duration) {
+ public enum Type {
+ ONCE,
+ POLL,
+ TTL,
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java
new file mode 100644
index 000000000..c27015318
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java
@@ -0,0 +1,89 @@
+package io.gravitee.node.api.secrets.runtime.spec;
+
+import static io.gravitee.node.api.secrets.runtime.discovery.Ref.formatUriAndKey;
+
+import io.gravitee.common.secrets.ValueKind;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Spec(
+ String id,
+ String name,
+ String uri,
+ String key,
+ List children,
+ boolean usesDynamicKey,
+ boolean isOnTheFly,
+ Resolution resolution,
+ ACLs acls,
+ String envId
+) {
+ public boolean hasChildren() {
+ return children != null && !children.isEmpty();
+ }
+
+ public Optional findChildrenFromName(String query) {
+ if (hasChildren()) {
+ return children.stream().filter(child -> Objects.equals(child.name(), query)).findFirst();
+ }
+ return Optional.empty();
+ }
+
+ public Optional findChildrenFromUri(String query) {
+ if (hasChildren()) {
+ return children.stream().filter(child -> Objects.equals(child.uri(), query)).findFirst();
+ }
+ return Optional.empty();
+ }
+
+ public String uriAndKey() {
+ return formatUriAndKey(uri, key);
+ }
+
+ public SecretURL toSecretURL() {
+ return SecretURL.from(uriAndKey(), false);
+ }
+
+ public String naturalId() {
+ return name != null && !name.isEmpty() ? name : uri;
+ }
+
+ public ValueKind valueKind() {
+ return acls != null ? acls.valueKind() : null;
+ }
+
+ public Set allowedFields() {
+ if (acls != null && acls.plugins() != null) {
+ return acls()
+ .plugins()
+ .stream()
+ .flatMap(pl -> {
+ if (pl.fields() == null) {
+ return Stream.empty();
+ }
+ return pl.fields().stream();
+ })
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+ }
+ return Set.of();
+ }
+
+ public record ChildSpec(String name, String uri, String key) {}
+
+ public boolean hasResolutionType(Resolution.Type type) {
+ if (type == Resolution.Type.ONCE) {
+ return resolution == null || resolution.type() == Resolution.Type.ONCE;
+ }
+ return resolution != null && type.equals(resolution.type());
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/SpecLifecycleService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/SpecLifecycleService.java
new file mode 100644
index 000000000..c6a5d53cb
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/SpecLifecycleService.java
@@ -0,0 +1,16 @@
+package io.gravitee.node.api.secrets.runtime.spec;
+
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface SpecLifecycleService {
+ boolean shouldDeployOnTheFly(Ref ref);
+ Spec deployOnTheFly(String envId, Ref ref, boolean retryOnError);
+
+ void deploy(Spec spec);
+
+ void undeploy(Spec spec);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Cache.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Cache.java
new file mode 100644
index 000000000..d4ab9bf14
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Cache.java
@@ -0,0 +1,22 @@
+package io.gravitee.node.api.secrets.runtime.storage;
+
+import io.gravitee.node.api.secrets.model.Secret;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public interface Cache {
+ void put(CacheKey cacheKey, Entry value);
+
+ void putPartial(CacheKey cacheKey, Map partial);
+
+ Optional get(CacheKey cacheKey);
+
+ void computeIfAbsent(CacheKey cacheKey, Supplier supplier);
+
+ void evict(CacheKey cacheKey);
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/CacheKey.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/CacheKey.java
new file mode 100644
index 000000000..c4499ab21
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/CacheKey.java
@@ -0,0 +1,23 @@
+package io.gravitee.node.api.secrets.runtime.storage;
+
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record CacheKey(String envId, String uri) implements Comparable {
+ @Override
+ public String toString() {
+ return envId + "-" + uri;
+ }
+
+ public static CacheKey from(Spec spec) {
+ return new CacheKey(spec.envId(), spec.uri());
+ }
+
+ @Override
+ public int compareTo(CacheKey o) {
+ return this.toString().compareTo(o.toString());
+ }
+}
diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Entry.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Entry.java
new file mode 100644
index 000000000..02ec4eb14
--- /dev/null
+++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Entry.java
@@ -0,0 +1,21 @@
+package io.gravitee.node.api.secrets.runtime.storage;
+
+import io.gravitee.node.api.secrets.model.Secret;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Entry(Type type, Map value, String error) {
+ public Entry {
+ value = value != null ? new HashMap<>(value) : new HashMap<>();
+ }
+ public enum Type {
+ VALUE,
+ EMPTY,
+ NOT_FOUND,
+ ERROR,
+ }
+}
diff --git a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMapTest.java b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMapTest.java
index c5ca74f73..698dff479 100644
--- a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMapTest.java
+++ b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMapTest.java
@@ -37,9 +37,9 @@ public static Stream secretMaps() {
@ParameterizedTest(name = "{0}")
@MethodSource("secretMaps")
void should_get_secret_from_map(String name, SecretMap secretMap) {
- SecretMount pass = new SecretMount(null, null, KEY, null);
+ SecretMount pass = new SecretMount(null, null, KEY, null, true);
assertThat(secretMap.getSecret(pass)).isPresent().get().extracting(Secret::asString).isEqualTo(SECRET);
- assertThat(secretMap.getSecret(new SecretMount(null, null, "bar", null))).isNotPresent();
+ assertThat(secretMap.getSecret(new SecretMount(null, null, "bar", null, true))).isNotPresent();
}
@Test
@@ -58,7 +58,7 @@ void should_have_expireAt_set() {
void should_have_well_know_data() {
SecretMap secretMap = SecretMap.of(Map.of(KEY, SECRET));
secretMap.handleWellKnownSecretKeys(Map.of(KEY, SecretMap.WellKnownSecretKey.PASSWORD));
- assertThat(secretMap.getSecret(new SecretMount(null, null, KEY, null)))
+ assertThat(secretMap.getSecret(new SecretMount(null, null, KEY, null, true)))
.isPresent()
.get()
.extracting(Secret::asString)
diff --git a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMountTest.java b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMountTest.java
index 8f6302e49..5439ffec0 100644
--- a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMountTest.java
+++ b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretMountTest.java
@@ -1,7 +1,6 @@
package io.gravitee.node.api.secrets.model;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
@@ -16,11 +15,11 @@ class SecretMountTest {
@Test
void should_know_if_key_is_empty() {
- SecretMount secretMount = new SecretMount(null, null, null, null);
+ SecretMount secretMount = new SecretMount(null, null, null, null, true);
assertThat(secretMount.isKeyEmpty()).isTrue();
- secretMount = new SecretMount(null, null, "", null);
+ secretMount = new SecretMount(null, null, "", null, true);
assertThat(secretMount.isKeyEmpty()).isTrue();
- secretMount = new SecretMount(null, null, "foo", null);
+ secretMount = new SecretMount(null, null, "foo", null, true);
assertThat(secretMount.isKeyEmpty()).isFalse();
}
}
diff --git a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretURLTest.java b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretURLTest.java
index f59488e0d..0fa59fc61 100644
--- a/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretURLTest.java
+++ b/gravitee-node-api/src/test/java/io/gravitee/node/api/secrets/model/SecretURLTest.java
@@ -3,14 +3,14 @@
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;
-import java.util.*;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
/**
@@ -56,7 +56,7 @@ static Stream workingURLs() {
@ParameterizedTest
@MethodSource("workingURLs")
void should_parse_url(String url, String provider, String path, String key, Map> query, boolean watch) {
- SecretURL cut = SecretURL.from(url);
+ SecretURL cut = SecretURL.from(url, true);
assertThat(cut.provider()).isEqualTo(provider);
assertThat(cut.path()).isEqualTo(path);
assertThat(cut.key()).isEqualTo(key);
@@ -84,7 +84,7 @@ static Stream nonWorkingURLs() {
@ParameterizedTest
@MethodSource("nonWorkingURLs")
void should_not_parse_url(String url) {
- assertThatCode(() -> SecretURL.from(url))
+ assertThatCode(() -> SecretURL.from(url, true))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("should have the following format");
}
@@ -120,7 +120,7 @@ public static Stream wellKnowKeysURLs() {
@ParameterizedTest
@MethodSource("wellKnowKeysURLs")
void should_have_well_known_mapping(String url, Map expected) {
- SecretURL cut = SecretURL.from(url);
+ SecretURL cut = SecretURL.from(url, true);
assertThat(cut.wellKnowKeyMap()).containsAllEntriesOf(expected);
}
@@ -140,7 +140,7 @@ public static Stream failingWellKnownKeysURLs() {
@ParameterizedTest
@MethodSource("failingWellKnownKeysURLs")
void should_fail_parsing_well_known_mapping_error(String url) {
- SecretURL cut = SecretURL.from(url);
+ SecretURL cut = SecretURL.from(url, true);
assertThatThrownBy(cut::wellKnowKeyMap).isInstanceOf(IllegalArgumentException.class);
}
}
diff --git a/gravitee-node-container/pom.xml b/gravitee-node-container/pom.xml
index 5b218238f..6cdb91abe 100644
--- a/gravitee-node-container/pom.xml
+++ b/gravitee-node-container/pom.xml
@@ -91,6 +91,12 @@
${project.version}
+
+ io.gravitee.node
+ gravitee-node-secrets-runtime
+ ${project.version}
+
+
io.gravitee.common
gravitee-common
diff --git a/gravitee-node-container/src/main/java/io/gravitee/node/container/AbstractNode.java b/gravitee-node-container/src/main/java/io/gravitee/node/container/AbstractNode.java
index abf9a47e1..efd1e0308 100644
--- a/gravitee-node-container/src/main/java/io/gravitee/node/container/AbstractNode.java
+++ b/gravitee-node-container/src/main/java/io/gravitee/node/container/AbstractNode.java
@@ -15,6 +15,7 @@
*/
package io.gravitee.node.container;
+import com.graviteesource.services.runtimesecrets.RuntimeSecretsService;
import io.gravitee.common.component.Lifecycle;
import io.gravitee.common.component.LifecycleComponent;
import io.gravitee.common.service.AbstractService;
@@ -145,6 +146,7 @@ public List> components() {
components.add(NodeHealthCheckService.class);
components.add(NodeMonitorService.class);
components.add(ReporterManager.class);
+ components.add(RuntimeSecretsService.class);
return components;
}
diff --git a/gravitee-node-container/src/main/java/io/gravitee/node/container/spring/SpringBasedContainer.java b/gravitee-node-container/src/main/java/io/gravitee/node/container/spring/SpringBasedContainer.java
index 46eba8507..9acb0969f 100644
--- a/gravitee-node-container/src/main/java/io/gravitee/node/container/spring/SpringBasedContainer.java
+++ b/gravitee-node-container/src/main/java/io/gravitee/node/container/spring/SpringBasedContainer.java
@@ -15,6 +15,7 @@
*/
package io.gravitee.node.container.spring;
+import com.graviteesource.services.runtimesecrets.spring.RuntimeSecretsBeanFactory;
import io.gravitee.common.component.LifecycleComponent;
import io.gravitee.kubernetes.client.spring.KubernetesClientConfiguration;
import io.gravitee.node.api.Node;
@@ -160,6 +161,7 @@ protected List> annotatedClasses() {
classes.add(PluginConfiguration.class);
classes.add(ManagementConfiguration.class);
classes.add(NodeMonitoringConfiguration.class);
+ classes.add(RuntimeSecretsBeanFactory.class);
// Bean registry post processor needs to be manually registered as it MUST be taken in account before spring context is refreshed.
classes.add(PluginHandlerBeanRegistryPostProcessor.class);
diff --git a/gravitee-node-license/src/main/java/io/gravitee/node/license/DefaultLicenseManager.java b/gravitee-node-license/src/main/java/io/gravitee/node/license/DefaultLicenseManager.java
index 8472ed8fa..25649decd 100644
--- a/gravitee-node-license/src/main/java/io/gravitee/node/license/DefaultLicenseManager.java
+++ b/gravitee-node-license/src/main/java/io/gravitee/node/license/DefaultLicenseManager.java
@@ -1,7 +1,9 @@
package io.gravitee.node.license;
import io.gravitee.common.service.AbstractService;
-import io.gravitee.node.api.license.*;
+import io.gravitee.node.api.license.ForbiddenFeatureException;
+import io.gravitee.node.api.license.License;
+import io.gravitee.node.api.license.LicenseManager;
import io.gravitee.plugin.core.api.PluginRegistry;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -13,7 +15,7 @@
/**
* This default {@link LicenseManager} is responsible for keeping a reference on the platform and the organizations licenses.
* It allows to easily validate a feature is allowed by the license.
- * Internally, the expiration date of the license is checked regularly.
+ * Internally, the pollInterval date of the license is checked regularly.
* Anyone can register an action to execute when a license is expired
*
* @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
diff --git a/gravitee-node-license/src/test/java/io/gravitee/node/license/DefaultLicenseFactoryTest.java b/gravitee-node-license/src/test/java/io/gravitee/node/license/DefaultLicenseFactoryTest.java
index 8c87a764c..e54a48bb2 100644
--- a/gravitee-node-license/src/test/java/io/gravitee/node/license/DefaultLicenseFactoryTest.java
+++ b/gravitee-node-license/src/test/java/io/gravitee/node/license/DefaultLicenseFactoryTest.java
@@ -355,7 +355,7 @@ private void assertUniverseLicense(License license) {
assertThat(license.isFeatureEnabled(feature)).isTrue();
}
- // Check expiration date is ok.
+ // Check pollInterval date is ok.
assertThat(license.getExpirationDate()).isAfter(Instant.now());
// Assert other attributes and raw.
diff --git a/gravitee-node-secrets/gravitee-node-secrets-plugin-handler/src/test/java/io/gravitee/node/secrets/plugins/internal/test/TestSecretProvider.java b/gravitee-node-secrets/gravitee-node-secrets-plugin-handler/src/test/java/io/gravitee/node/secrets/plugins/internal/test/TestSecretProvider.java
index 1640a274e..db3af062f 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-plugin-handler/src/test/java/io/gravitee/node/secrets/plugins/internal/test/TestSecretProvider.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-plugin-handler/src/test/java/io/gravitee/node/secrets/plugins/internal/test/TestSecretProvider.java
@@ -20,6 +20,6 @@ public Flowable watch(SecretMount secretMount) {
@Override
public SecretMount fromURL(SecretURL url) {
- return new SecretMount("test", new SecretLocation(), "password", url);
+ return new SecretMount("test", new SecretLocation(), "password", url, true);
}
}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml
new file mode 100644
index 000000000..31cfc1ff4
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml
@@ -0,0 +1,96 @@
+
+
+
+ 4.0.0
+
+ io.gravitee.node
+ gravitee-node-secrets
+ 6.4.4
+
+
+ gravitee-node-secrets-runtime
+ Gravitee.io - Node - Secrets - Runtime
+
+
+ 3.2.3
+
+
+
+
+ org.springframework
+ spring-core
+
+
+ io.gravitee.node
+ gravitee-node-api
+
+
+ io.gravitee.node
+ gravitee-node-secrets-service
+
+
+ io.gravitee.node
+ gravitee-node-secrets-plugin-handler
+
+
+ io.reactivex.rxjava3
+ rxjava
+
+
+ io.gravitee.el
+ gravitee-expression-language
+ ${gravitee-expression-language.version}
+
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ io.gravitee.node
+ gravitee-secret-provider-mock
+ ${project.version}
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.yaml
+ snakeyaml
+ ${snakeyaml.version}
+ test
+
+
+ org.springframework.security
+ spring-security-core
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java
new file mode 100644
index 000000000..3f098e972
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets;
+
+import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry;
+import com.graviteesource.services.runtimesecrets.discovery.PayloadRefParser;
+import com.graviteesource.services.runtimesecrets.discovery.RefParser;
+import com.graviteesource.services.runtimesecrets.el.Formatter;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import io.gravitee.node.api.secrets.runtime.discovery.*;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import java.util.*;
+import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class Processor {
+
+ private final DefinitionBrowserRegistry definitionBrowserRegistry;
+ private final ContextRegistry contextRegistry;
+ private final SpecRegistry specRegistry;
+ private final GrantService grantService;
+ private final SpecLifecycleService specLifecycleService;
+
+ /**
+ * finds a {@link DefinitionBrowser}
+ * Run it to get {@link DiscoveryContext}
+ * Inject EL {@link PayloadRefParser}
+ * Find {@link Spec} or create on the fly
+ * Grant {@link DiscoveryContext}
+ * @param definition the secret ref container
+ * @param metadata some optional metadata
+ * @param the kind of subject
+ */
+ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) {
+ Optional> browser = getDefinitionBrowser(definition);
+ if (browser.isEmpty()) {
+ return;
+ }
+
+ DefinitionBrowser definitionBrowser = browser.get();
+ Definition rootDefinition = definitionBrowser.getDefinitionLocation(definition, metadata);
+
+ log.info("Finding secret in definition: {}", rootDefinition);
+
+ DefaultPayloadNotifier notifier = new DefaultPayloadNotifier(rootDefinition, envId, specRegistry);
+ definitionBrowser.findPayloads(definition, notifier);
+
+ // register contexts by ref and definition
+ for (DiscoveryContext context : notifier.getContextList()) {
+ contextRegistry.register(context, rootDefinition);
+ // get spec
+ Spec spec = specRegistry.fromRef(context.envId(), context.ref());
+ if (spec == null && specLifecycleService.shouldDeployOnTheFly(context.ref())) {
+ spec = specLifecycleService.deployOnTheFly(envId, context.ref(), true);
+ }
+
+ if (context.ref().mainExpression().isLiteral()) {
+ boolean granted = grantService.grant(context, spec);
+ if (granted) {
+ grantService.grant(context, spec);
+ }
+ }
+ }
+ }
+
+ private Optional> getDefinitionBrowser(T definition) {
+ Optional> browser = definitionBrowserRegistry.findBrowser(definition);
+ if (browser.isEmpty()) {
+ log.info("No definition browser found for kind [{}]", definition.getClass());
+ }
+ return browser;
+ }
+
+ public void onDefinitionUnDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) {
+ Optional> browser = getDefinitionBrowser(definition);
+ if (browser.isEmpty()) {
+ return;
+ }
+
+ Definition rootDefinition = browser.get().getDefinitionLocation(definition, metadata);
+ List currentDefinitionContexts = contextRegistry.getByDefinition(envId, rootDefinition);
+
+ // undeploy unused on-the-fly spec
+ record ContextBySpec(Spec spec, long count) {}
+ currentDefinitionContexts
+ .stream()
+ .map(context -> specRegistry.fromRef(envId, context.ref()))
+ .filter(Spec::isOnTheFly)
+ .map(spec -> {
+ // count context using this spec, excluding the current definition's
+ long count = contextRegistry
+ .findBySpec(spec)
+ .stream()
+ .filter(context1 -> !currentDefinitionContexts.contains(context1))
+ .count();
+ return new ContextBySpec(spec, count);
+ })
+ // when this spec will not be used after definition is undeploy
+ .filter(contextBySpec -> contextBySpec.count == 0L)
+ .map(ContextBySpec::spec)
+ .forEach(specLifecycleService::undeploy);
+
+ // revoke and remove all contexts
+ currentDefinitionContexts.forEach(context -> {
+ grantService.revoke(context);
+ contextRegistry.unregister(context, rootDefinition);
+ });
+ }
+
+ static final class DefaultPayloadNotifier implements DefinitionPayloadNotifier {
+
+ @Getter
+ final List contextList = new ArrayList<>();
+
+ final Definition rootDefinition;
+ final String envId;
+ final SpecRegistry specRegistry;
+
+ DefaultPayloadNotifier(Definition rootDefinition, String envId, SpecRegistry specRegistry) {
+ this.rootDefinition = rootDefinition;
+ this.envId = envId;
+ this.specRegistry = specRegistry;
+ }
+
+ @Override
+ public void onPayload(String payload, PayloadLocation payloadLocation, Consumer updatedPayload) {
+ // no op on empty payloads
+ if (payload == null || payload.isBlank()) {
+ updatedPayload.accept(payload);
+ return;
+ }
+ PayloadRefParser payloadRefParser = new PayloadRefParser(payload);
+ List discoveryContexts = payloadRefParser
+ .runDiscovery()
+ .stream()
+ .map(raw -> RefParser.parse(raw.ref()))
+ .map(ref ->
+ new DiscoveryContext(
+ UUID.randomUUID(),
+ envId,
+ ref,
+ new DiscoveryLocation(
+ new DiscoveryLocation.Definition(this.rootDefinition.kind(), this.rootDefinition.id()),
+ payloadLocation
+ )
+ )
+ )
+ .toList();
+ contextList.addAll(discoveryContexts);
+ List ELs = discoveryContexts
+ .stream()
+ .map(context -> {
+ if (context.ref().mainExpression().isLiteral()) {
+ return Formatter.computeELFromStatic(context, envId);
+ } else {
+ return Formatter.computeELFromEL(context, envId);
+ }
+ })
+ .toList();
+ payloadRefParser.replaceRefs(ELs);
+ updatedPayload.accept(payloadRefParser.getUpdatePayload());
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsService.java
new file mode 100644
index 000000000..c45595e02
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsService.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets;
+
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import io.gravitee.common.secrets.ValueKind;
+import io.gravitee.common.service.AbstractService;
+import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer;
+import io.gravitee.node.api.secrets.runtime.spec.ACLs;
+import io.gravitee.node.api.secrets.runtime.spec.Resolution;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.*;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.env.Environment;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RequiredArgsConstructor
+public class RuntimeSecretsService extends AbstractService {
+
+ private final Processor processor;
+ private final SpecLifecycleService specLifecycleService;
+ private final SpecRegistry specRegistry;
+ private final SecretProviderDeployer secretProviderDeployer;
+ private final RenewalService renewalService;
+ private final Environment environment;
+
+ @Override
+ protected void doStart() throws Exception {
+ secretProviderDeployer.init();
+ renewalService.start();
+ startDemo();
+ }
+
+ public void deploy(Spec spec) {
+ specLifecycleService.deploy(spec);
+ }
+
+ public void undeploy(Spec spec) {
+ specLifecycleService.undeploy(spec);
+ }
+
+ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) {
+ processor.onDefinitionDeploy(envId, definition, metadata);
+ }
+
+ public void onDefinitionUnDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) {
+ processor.onDefinitionUnDeploy(envId, definition, metadata);
+ }
+
+ private void startDemo() {
+ startWatch(environment.getProperty("rtsecdemodir"));
+ specLifecycleService.deploy(
+ new Spec(
+ "a9c0ea5b-aac8-4064-bed5-47021082f8a2",
+ "dyn-api-keys",
+ "/mock/dynamic-key/named/apikeys",
+ null,
+ null,
+ true,
+ false,
+ null,
+ acls("transform-headers"),
+ "DEFAULT"
+ )
+ );
+ specLifecycleService.deploy(
+ new Spec(
+ "49a538ff-ac6e-4534-b3d1-ab37519569e4",
+ "renewable-api-keys",
+ "/mock/rotating",
+ "api-key-1",
+ null,
+ true,
+ false,
+ new Resolution(Resolution.Type.POLL, Duration.ofSeconds(5)),
+ acls("transform-headers"),
+ "DEFAULT"
+ )
+ );
+ }
+
+ private void startWatch(String directory) {
+ if (directory == null) {
+ return;
+ }
+ Path watched = Path.of(directory);
+ if (!Files.exists(watched)) {
+ return;
+ }
+
+ final WatchService watcherService;
+ try {
+ watcherService = FileSystems.getDefault().newWatchService();
+ watched.register(watcherService, StandardWatchEventKinds.ENTRY_MODIFY);
+ Schedulers
+ .newThread()
+ .scheduleDirect(() -> {
+ while (true) {
+ WatchKey watchKey = watcherService.poll();
+ if (watchKey == null) {
+ continue;
+ }
+ final Optional path = watchKey
+ .pollEvents()
+ .stream()
+ .map(watchEvent -> ((WatchEvent) watchEvent).context())
+ .filter(file -> file.toString().endsWith(".properties"))
+ .findFirst();
+
+ if (path.isPresent()) {
+ Properties properties = new Properties();
+ try {
+ properties.load(new FileReader(watched.resolve(path.get()).toFile()));
+ handleDemo(properties);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ if (!watchKey.reset()) {
+ break;
+ }
+ }
+ });
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void handleDemo(Properties properties) {
+ String pluginToAdd = properties.getProperty("updateSpecACLAddPlugin", "ignore");
+ String fieldToAdd = properties.getProperty("updateSpecACLAddField", "");
+ String valueKind = properties.getProperty("updateSpecACLSetKind", "");
+ String specToUndeploy = properties.getProperty("undeploySpec", "");
+ String otfSpecWithACL = properties.getProperty("otfSpecWithACL", "");
+ int otfSpecWithRenewal = Integer.parseInt(properties.getProperty("otfSpecWithRenewal", "0"));
+
+ if (!otfSpecWithACL.isEmpty()) {
+ specLifecycleService.deploy(
+ new Spec(
+ "f9024ec8-ad20-4834-8962-9c9153218983",
+ "case-1-api-key",
+ "/mock/case1",
+ "api-key",
+ null,
+ false,
+ false,
+ otfSpecWithRenewal > 0 ? new Resolution(Resolution.Type.POLL, Duration.ofSeconds(otfSpecWithRenewal)) : null,
+ acls(otfSpecWithACL),
+ "DEFAULT"
+ )
+ );
+ }
+ if (!pluginToAdd.equals("ignore")) {
+ deployStaticApiKey(pluginToAdd, valueKind, fieldToAdd);
+ }
+
+ if (!specToUndeploy.isEmpty()) {
+ Spec spec = specRegistry.getFromName("DEFAULT", specToUndeploy);
+ if (spec != null) {
+ specLifecycleService.undeploy(spec);
+ }
+ }
+ }
+
+ private void deployStaticApiKey(String pluginToAdd, String valueKind, String fieldToAdd) {
+ specLifecycleService.deploy(
+ new Spec(
+ "e69328d2-cdb0-4970-a94e-c521ff03f1d5",
+ "case2-api-key",
+ "/mock/case2",
+ "api-key",
+ null,
+ false,
+ false,
+ null,
+ acls(pluginToAdd, valueKind, fieldToAdd),
+ "DEFAULT"
+ )
+ );
+ }
+
+ private ACLs acls(String pluginToAdd) {
+ return acls(pluginToAdd, "", "");
+ }
+
+ private ACLs acls(String pluginToAdd, String valueKind, String fieldToAdd) {
+ if (pluginToAdd.isEmpty()) {
+ return null;
+ }
+ return new ACLs(
+ valueKind.isEmpty() ? null : ValueKind.valueOf(valueKind.toUpperCase()),
+ null,
+ List.of(new ACLs.PluginACL(pluginToAdd, fieldToAdd.isEmpty() ? null : List.of(fieldToAdd)))
+ );
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Config.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Config.java
new file mode 100644
index 000000000..a3790688b
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Config.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.config;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Config(OnTheFlySpecs onTheFlySpecs, Retry retry, Renewal renewal) {
+ public static final String CONFIG_PREFIX = "api.secrets";
+ public static final String ON_THE_FLY_SPECS_ENABLED = CONFIG_PREFIX + ".onTheFlySpecs.enabled";
+ public static final String ON_THE_FLY_SPECS_ON_ERROR_RETRY_AFTER_DELAY = CONFIG_PREFIX + ".onTheFlySpecs.onErrorRetryAfter.delay";
+ public static final String ON_THE_FLY_SPECS_ON_ERROR_RETRY_AFTER_UNIT = CONFIG_PREFIX + ".onTheFlySpecs.onErrorRetryAfter.unit";
+ public static final String RENEWAL_ENABLED = CONFIG_PREFIX + ".renewal.enable";
+ public static final String RENEWAL_CHECK_DELAY = CONFIG_PREFIX + ".renewal.check.delay";
+ public static final String RENEWAL_CHECK_UNIT = CONFIG_PREFIX + ".renewal.check.unit";
+ public static final String RETRY_ON_ERROR_ENABLED = CONFIG_PREFIX + ".retryOnError.enable";
+ public static final String RETRY_ON_ERROR_DELAY = CONFIG_PREFIX + ".retryOnError.delay";
+ public static final String RETRY_ON_ERROR_UNIT = CONFIG_PREFIX + ".retryOnError.unit";
+ public static final String RETRY_ON_ERROR_BACKOFF_FACTOR = CONFIG_PREFIX + ".retryOnError.backoffFactor";
+ public static final String RETRY_ON_ERROR_BACKOFF_MAX_DELAY = CONFIG_PREFIX + ".retryOnError.maxDelay";
+ public static final String API_SECRETS_ALLOW_PROVIDERS_FROM_CONFIGURATION = CONFIG_PREFIX + ".allowProvidersFromConfiguration";
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/OnTheFlySpecs.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/OnTheFlySpecs.java
new file mode 100644
index 000000000..384452299
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/OnTheFlySpecs.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.config;
+
+import java.time.Duration;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record OnTheFlySpecs(boolean enabled, Duration onErrorRetryAfter) {}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Renewal.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Renewal.java
new file mode 100644
index 000000000..cf0570645
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Renewal.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.config;
+
+import java.time.Duration;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Renewal(boolean enabled, Duration duration) {}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Retry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Retry.java
new file mode 100644
index 000000000..c5f282264
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Retry.java
@@ -0,0 +1,12 @@
+package com.graviteesource.services.runtimesecrets.config;
+
+import java.util.concurrent.TimeUnit;/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+public record Retry(boolean enabled, long delay, TimeUnit unit, float backoffFactor, int maxDelay) {
+ public static Retry none() {
+ return new Retry(false, 0, TimeUnit.MILLISECONDS, 0, 0);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java
new file mode 100644
index 000000000..4b8f81387
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import io.gravitee.node.api.secrets.runtime.discovery.ContextRegistry;
+import io.gravitee.node.api.secrets.runtime.discovery.Definition;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class DefaultContextRegistry implements ContextRegistry {
+
+ Map registry = new HashMap<>();
+
+ public void register(DiscoveryContext context, Definition definition) {
+ registry(context.envId()).register(context, definition);
+ }
+
+ public List findBySpec(Spec spec) {
+ return registry(spec.envId()).findBySpec(spec);
+ }
+
+ public List getByDefinition(String envId, Definition definition) {
+ return registry(envId).getByDefinition(null, definition);
+ }
+
+ @Override
+ public void unregister(DiscoveryContext context, Definition definition) {
+ registry(context.envId()).unregister(context, definition);
+ }
+
+ ContextRegistry registry(String envId) {
+ return registry.computeIfAbsent(envId, ignore -> new InternalRegistry());
+ }
+
+ static class InternalRegistry implements ContextRegistry {
+
+ private final Multimap byName = MultimapBuilder.hashKeys().arrayListValues().build();
+ private final Multimap byUri = MultimapBuilder.hashKeys().arrayListValues().build();
+ private final Multimap byUriAndKey = MultimapBuilder.hashKeys().arrayListValues().build();
+ private final Multimap byDefinitionSpec = MultimapBuilder.hashKeys().arrayListValues().build();
+
+ public void register(DiscoveryContext context, Definition definition) {
+ if (context.ref().mainType() == Ref.MainType.NAME && context.ref().mainExpression().isLiteral()) {
+ synchronized (byName) {
+ byName.put(context.ref().mainExpression().value(), context);
+ }
+ }
+ if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) {
+ synchronized (byUri) {
+ byUri.put(context.ref().mainExpression().value(), context);
+ }
+
+ if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) {
+ synchronized (byUriAndKey) {
+ byUriAndKey.put(context.ref().uriAndKey(), context);
+ }
+ }
+ }
+ synchronized (byDefinitionSpec) {
+ byDefinitionSpec.put(definition, context);
+ }
+ }
+
+ public List findBySpec(Spec spec) {
+ List result = new ArrayList<>();
+ if (spec.name() != null && !spec.name().isEmpty()) {
+ synchronized (byName) {
+ result.addAll(byName.get(spec.name()));
+ }
+ }
+ if (spec.uri() != null && !spec.uri().isEmpty()) {
+ synchronized (byUri) {
+ result.addAll(byUri.get(spec.name()));
+ }
+ }
+ if (spec.key() != null && !spec.key().isEmpty()) {
+ synchronized (byUriAndKey) {
+ result.addAll(byUriAndKey.get(spec.uriAndKey()));
+ }
+ }
+ return result;
+ }
+
+ public List getByDefinition(String envId, Definition definition) {
+ synchronized (byDefinitionSpec) {
+ return List.copyOf(byDefinitionSpec.get(definition));
+ }
+ }
+
+ @Override
+ public void unregister(DiscoveryContext context, Definition definition) {
+ if (context.ref().mainType() == Ref.MainType.NAME && context.ref().mainExpression().isLiteral()) {
+ synchronized (byName) {
+ byName.remove(context.ref().mainExpression().value(), context);
+ }
+ }
+ if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) {
+ synchronized (byUri) {
+ byUri.remove(context.ref().mainExpression().value(), context);
+ }
+ if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) {
+ synchronized (byUriAndKey) {
+ byUriAndKey.remove(context.ref().uriAndKey(), context);
+ }
+ }
+ }
+ synchronized (byDefinitionSpec) {
+ byDefinitionSpec.put(definition, context);
+ }
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefinitionBrowserRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefinitionBrowserRegistry.java
new file mode 100644
index 000000000..c7073977b
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefinitionBrowserRegistry.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import io.gravitee.node.api.secrets.runtime.discovery.DefinitionBrowser;
+import java.util.Collection;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class DefinitionBrowserRegistry {
+
+ private final Collection browsers;
+
+ @Autowired
+ public DefinitionBrowserRegistry(Collection browsers) {
+ this.browsers = browsers;
+ }
+
+ public Optional> findBrowser(T definition) {
+ for (DefinitionBrowser browser : this.browsers) {
+ if (browser.canHandle(definition)) {
+ return Optional.of(browser);
+ }
+ }
+ return Optional.empty();
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java
new file mode 100644
index 000000000..7f6bcebf9
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+public class PayloadRefParser {
+
+ private final StringBuilder payload;
+
+ @Getter(AccessLevel.PACKAGE)
+ private List rawRefs;
+
+ public PayloadRefParser(String payload) {
+ this.payload = new StringBuilder(payload);
+ }
+
+ @AllArgsConstructor
+ static class Position {
+
+ private int start;
+ private int end;
+
+ private void move(int quantity) {
+ start += quantity;
+ end += quantity;
+ }
+ }
+
+ public record RawSecretRef(String ref, Position position) {}
+
+ public List runDiscovery() {
+ int start = 0;
+ List result = new ArrayList<>();
+ do {
+ start = payload.indexOf(RefParser.BEGIN_SEPARATOR, start);
+ if (start >= 0) {
+ int end = payload.indexOf(RefParser.END_SEPARATOR, start);
+ int afterEnd = end + RefParser.END_SEPARATOR.length();
+ String ref = payload.substring(start, afterEnd);
+ result.add(new RawSecretRef(ref, new Position(start, afterEnd)));
+ start = afterEnd;
+ }
+ } while (start >= 0);
+ this.rawRefs = result;
+ return result;
+ }
+
+ public String replaceRefs(List expressions) {
+ if (expressions.size() != this.rawRefs.size()) {
+ throw new IllegalArgumentException("ref and replacement list don't match in size");
+ }
+
+ for (int i = 0; i < rawRefs.size(); i++) {
+ String replacement = expressions.get(i);
+
+ // replace ref by expression
+ Position position = rawRefs.get(i).position;
+ payload.replace(position.start, position.end, replacement);
+
+ // compute lengthDiff in position
+ int refStringLength = position.end - position.start;
+ int replacementLength = replacement.length();
+ int lengthDiff = replacementLength - refStringLength;
+ // apply offset change on next ref positions
+ for (int p = i + 1; p < expressions.size(); p++) {
+ rawRefs.get(p).position.move(lengthDiff);
+ }
+ }
+
+ return payload.toString();
+ }
+
+ public String getUpdatePayload() {
+ return payload.toString();
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/RefParser.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/RefParser.java
new file mode 100644
index 000000000..c4dc9fff9
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/RefParser.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import static io.gravitee.node.api.secrets.runtime.discovery.Ref.URI_KEY_SEPARATOR;
+
+import com.graviteesource.services.runtimesecrets.errors.SecretRefParsingException;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import java.util.List;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class RefParser {
+
+ public static final String BEGIN_SEPARATOR = "<<";
+ public static final String END_SEPARATOR = ">>";
+ private static final String NAME_TYPE = " name ";
+ private static final int MAX_MAIN_TYPE_SIZE = NAME_TYPE.length();
+ private static final String URI_TYPE = " uri ";
+ private static final String KEY_TYPE = " key ";
+
+ private static final List AFTER_MAIN_TOKENS = List.of(NAME_TYPE, URI_TYPE, KEY_TYPE, END_SEPARATOR);
+
+ private static final char EL_START_CB = '{';
+ private static final char EL_START_HASH = '#';
+ private static final List EL_START = List.of("" + EL_START_CB + EL_START_HASH, "" + EL_START_HASH);
+
+ /**
+ * << [name|uri] [EL_]EXPRESSION [[name | uri | key] [EL_]EXPRESSION] >>
+ *
+ * <space+>
= one or several spaces (\u0020
)
+ * BEGIN_SEPARATOR
"<<
" optionally followed by
+ * END_SEPARATOR
">>
" optionally preceded by
+ * MAIN_TYPE
is literal case-sensitive string: "uri
" or "name
" preceded AND followed by
+ * OPTIONAL_TYPE
extends MAIN_TYPE
with literal "key
"
+ * KEY
is a string or an EL_STRING
that designate the key to get from a secret map
+ * EXPRESSION
is any string NOT starting with '#
' or "{#
". When MAIN_TYPE
is absent or "uri
" then a EL_EXPRESSION
ending with ":
" and followed is an alias of "key KEY
"
+ * EL_EXPRESSION
= string starting with '#
' or "{#
". After parsing {
and }
is removed (a.k.a mixin).
+ *
+ * @param ref the full uri (with start and end separator)
+ * @return a SecretRef
+ * @throws SecretRefParsingException when parsing fails
+ */
+ public static Ref parse(String ref) {
+ if (ref == null || ref.isBlank()) {
+ throw new SecretRefParsingException("ref is null or empty");
+ }
+ var buffer = new StringBuilder(ref);
+ // delete
+ buffer.delete(0, BEGIN_SEPARATOR.length());
+
+ final String typeString = mainType(buffer);
+ final Ref.MainType mainType;
+ try {
+ mainType = Ref.MainType.valueOf(typeString.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw enumError("unknown kind: '%s' for secret reference '%s'".formatted(typeString, ref));
+ }
+ final RefParsing refParsing = mainExpression(buffer, ref);
+
+ Ref.SecondaryType secondaryType = null;
+ if (refParsing.secondaryType() != null) {
+ try {
+ secondaryType = Ref.SecondaryType.valueOf(refParsing.secondaryType().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw enumError("unknown kind: '%s' for secret reference '%s'".formatted(refParsing.secondaryType(), ref));
+ }
+ if (mainType == Ref.MainType.URI && secondaryType != Ref.SecondaryType.KEY) {
+ throw new SecretRefParsingException(
+ "reference of kind '%s' can only be followed by keyword '%s' or contain '%s' in reference %s".formatted(
+ URI_TYPE.trim(),
+ KEY_TYPE.trim(),
+ URI_KEY_SEPARATOR,
+ ref
+ )
+ );
+ }
+ if (isEL(refParsing.mainExpression)) {
+ throw new SecretRefParsingException(
+ "reference of kind '%s' is using an EL expression, it cannot be followed a keyword in reference %s".formatted(
+ URI_TYPE.trim(),
+ ref
+ )
+ );
+ }
+ }
+
+ return new Ref(
+ mainType,
+ toExpression(refParsing.mainExpression()),
+ secondaryType,
+ secondaryType != null ? toExpression(refParsing.secondaryExpression()) : null,
+ ref
+ );
+ }
+
+ private static String mainType(StringBuilder buffer) {
+ while (buffer.charAt(0) == ' ') {
+ buffer.delete(0, 1);
+ }
+ int typeEnd = buffer.indexOf(" ");
+ // reach end without spaces or bigger than a main kind
+ if (typeEnd == -1 || typeEnd > MAX_MAIN_TYPE_SIZE) {
+ if (buffer.charAt(0) == SecretURL.URL_SEPARATOR) {
+ return URI_TYPE.trim();
+ }
+ if (isEL(buffer.toString())) {
+ throw new SecretRefParsingException(
+ "EL expression must be preceded by '%s' or '%s' when located at the beginning of secret reference".formatted(
+ NAME_TYPE.stripLeading(),
+ URI_TYPE.stripLeading()
+ )
+ );
+ }
+ return NAME_TYPE.trim();
+ }
+ String type = buffer.substring(0, typeEnd);
+ buffer.delete(0, typeEnd);
+ return type;
+ }
+
+ record RefParsing(String mainExpression, String secondaryType, String secondaryExpression) {}
+
+ private static RefParsing mainExpression(StringBuilder buffer, String ref) {
+ String foundToken = null;
+ String expression = null;
+ int end = 0;
+ for (String token : AFTER_MAIN_TOKENS) {
+ end = buffer.indexOf(token);
+ if (end != -1) {
+ foundToken = token;
+ expression = buffer.substring(0, end);
+ buffer.delete(0, end);
+ break;
+ }
+ }
+
+ if (foundToken == null) {
+ throw new SecretRefParsingException("reference %s syntax is incorrect".formatted(ref));
+ }
+
+ String uriOrName = expression.trim();
+ String secondaryType = null;
+ String secondary = null;
+
+ if (!foundToken.equals(END_SEPARATOR)) {
+ secondary = buffer.substring(foundToken.length(), buffer.length() - END_SEPARATOR.length()).trim();
+ secondaryType = foundToken.trim();
+ }
+ if (!foundToken.equals(KEY_TYPE) && expression.contains(URI_KEY_SEPARATOR)) {
+ UriAndKey uriAndKey = parseUriAndKey(expression, end);
+ uriOrName = uriAndKey.uri();
+ secondaryType = KEY_TYPE.trim();
+ secondary = uriAndKey.key();
+ }
+
+ return new RefParsing(uriOrName, secondaryType, secondary);
+ }
+
+ public record UriAndKey(String uri, String key) {
+ public Ref asRef() {
+ return new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression(uri, false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression(key, false),
+ asString()
+ );
+ }
+
+ private String asString() {
+ return Ref.formatUriAndKey(uri, key);
+ }
+ }
+
+ public static UriAndKey parseUriAndKey(String expression) {
+ return parseUriAndKey(expression, expression.length());
+ }
+
+ public static UriAndKey parseUriAndKey(String expression, int end) {
+ int keyIndex = expression.indexOf(URI_KEY_SEPARATOR);
+ String uri = expression.substring(0, keyIndex).trim();
+ String key = expression.substring(keyIndex + 1, end).trim();
+ return new UriAndKey(uri, key);
+ }
+
+ private static boolean isEL(String buffer) {
+ if (buffer == null || buffer.isBlank()) {
+ return false;
+ }
+ for (String start : EL_START) {
+ if (buffer.indexOf(start) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Ref.Expression toExpression(String spec) {
+ if (isEL(spec)) {
+ return new Ref.Expression(cleanEL(spec), true);
+ }
+ return new Ref.Expression(spec, false);
+ }
+
+ private static String cleanEL(String el) {
+ if (el.charAt(0) == EL_START_CB) {
+ return el.substring(1, el.length() - 1);
+ }
+ return el;
+ }
+
+ private static SecretRefParsingException enumError(String typeString) {
+ return new SecretRefParsingException(typeString);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java
new file mode 100644
index 000000000..29b481702
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el;
+
+import static io.gravitee.common.secrets.RuntimeContext.EL_VARIABLE;
+
+import io.gravitee.node.api.secrets.runtime.discovery.Definition;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import java.util.UUID;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class Formatter {
+
+ public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s', " + "#" + EL_VARIABLE + ")}";
+ public static final String FROM_GRANT_EL_KEY_TEMPLATE = "{#secrets.fromGrant('%s', %s, " + "#" + EL_VARIABLE + ")}";
+ public static final String METHOD_NAME_SUFFIX = "WithName";
+ public static final String METHOD_URI_SUFFIX = "WithUri";
+ public static final String FROM_GRANT_WITH_TEMPLATE = "{#secrets.fromGrant%s('%s', '%s', '%s', %s)}";
+ public static final String FROM_EL_WITH_TEMPLATE = "{#secrets.fromEL%s('%s', %s, '%s', '%s'%s)}";
+
+ public static String computeELFromStatic(DiscoveryContext context, String envId) {
+ if (context.ref().mainExpression().isEL()) {
+ throw new IllegalArgumentException("mis-usage this method only supports main secret expression as a literal string");
+ }
+ final String mainSpec = context.ref().mainExpression().value();
+ String el;
+ if (context.ref().secondaryType() != null) {
+ switch (context.ref().secondaryType()) {
+ case KEY -> {
+ if (context.ref().secondaryExpression().isLiteral()) {
+ el = fromGrant(context.id());
+ } else {
+ el = fromGrant(context.id(), context.ref().secondaryExpression().value());
+ }
+ }
+ case NAME -> el =
+ fromGrantWithTemplate(METHOD_NAME_SUFFIX, context.id(), envId, mainSpec, context.ref().secondaryExpression());
+ case URI -> el =
+ fromGrantWithTemplate(METHOD_URI_SUFFIX, context.id(), envId, mainSpec, context.ref().secondaryExpression());
+ default -> {
+ throw new IllegalArgumentException("secondary type unknown: %s".formatted(context.ref().secondaryType()));
+ }
+ }
+ } else {
+ el = fromGrant(context.id());
+ }
+ return el;
+ }
+
+ public static String computeELFromEL(DiscoveryContext context, String envId, Definition... others) {
+ if (context.ref().mainExpression().isLiteral()) {
+ throw new IllegalArgumentException("mis-usage this method only supports main secret expression as an EL");
+ }
+ String el;
+ switch (context.ref().mainType()) {
+ case NAME -> el =
+ FROM_EL_WITH_TEMPLATE.formatted(
+ METHOD_NAME_SUFFIX,
+ envId,
+ context.ref().mainExpression().value(),
+ context.location().definition().kind(),
+ context.location().definition().id(),
+ others(context.location().payloadLocations())
+ );
+ case URI -> el =
+ FROM_EL_WITH_TEMPLATE.formatted(
+ METHOD_URI_SUFFIX,
+ envId,
+ context.ref().mainExpression().value(),
+ context.location().definition().kind(),
+ context.location().definition().id(),
+ others(context.location().payloadLocations())
+ );
+ default -> {
+ throw new IllegalArgumentException("main type unknown: %s".formatted(context.ref().secondaryType()));
+ }
+ }
+ return el;
+ }
+
+ private static String others(PayloadLocation... ignoredTODO) {
+ return "";
+ }
+
+ private static String fromGrantWithTemplate(
+ String methodSuffix,
+ UUID id,
+ String envId,
+ String literalExpression,
+ Ref.Expression secondaryExpression
+ ) {
+ return FROM_GRANT_WITH_TEMPLATE.formatted(methodSuffix, id, envId, literalExpression, quoteLiteral(secondaryExpression));
+ }
+
+ private static String fromGrant(UUID id) {
+ return FROM_GRANT_TEMPLATE.formatted(id);
+ }
+
+ private static String fromGrant(UUID id, String key) {
+ return FROM_GRANT_EL_KEY_TEMPLATE.formatted(id, key);
+ }
+
+ private static String quoteLiteral(Ref.Expression expression) {
+ // literal string or EL (as is)
+ return expression.isLiteral() ? "'%s'".formatted(expression.value()) : expression.value();
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Result.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Result.java
new file mode 100644
index 000000000..89c1f0512
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Result.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Result(Type type, String value) {
+ enum Type {
+ VALUE,
+ EMPTY,
+ NOT_FOUND,
+ KEY_NOT_FOUND,
+ DENIED,
+ ERROR,
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Service.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Service.java
new file mode 100644
index 000000000..41d8f8779
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Service.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el;
+
+import static io.gravitee.node.api.secrets.runtime.discovery.Ref.URI_KEY_SEPARATOR;
+
+import com.graviteesource.services.runtimesecrets.discovery.RefParser;
+import com.graviteesource.services.runtimesecrets.errors.*;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import io.gravitee.common.secrets.RuntimeContext;
+import io.gravitee.node.api.secrets.model.Secret;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryLocation;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.grant.Grant;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import java.util.Map;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RequiredArgsConstructor
+public class Service {
+
+ private final Cache cache;
+ private final GrantService grantService;
+ private final SpecLifecycleService specLifecycleService;
+ private final SpecRegistry specRegistry;
+
+ // TODO add a lambda to handle TTL
+
+ public String fromGrant(String contextId, RuntimeContext runtimeContext) {
+ Optional grantOptional = grantService.getGrant(contextId);
+ if (grantOptional.isEmpty() || !grantOptional.get().match(runtimeContext)) {
+ return resultToValue(new Result(Result.Type.DENIED, "secret is denied"));
+ }
+ return getFromCache(grantOptional.get(), grantOptional.get().secretKey());
+ }
+
+ public String fromGrant(String contextId, String secretKey, RuntimeContext runtimeContext) {
+ Optional grantOptional = grantService.getGrant(contextId);
+ if (grantOptional.isEmpty() || !grantOptional.get().match(runtimeContext)) {
+ return resultToValue(new Result(Result.Type.DENIED, "secret is denied"));
+ }
+ return getFromCache(grantOptional.get(), secretKey);
+ }
+
+ private String getFromCache(Grant grant, String key) {
+ return resultToValue(
+ toResult(
+ cache
+ .get(grant.cacheKey())
+ .orElse(
+ new Entry(
+ Entry.Type.EMPTY,
+ null,
+ "no value in cache for [%s] in environment [%s]".formatted(grant.cacheKey().uri(), grant.cacheKey().envId())
+ )
+ ),
+ key
+ )
+ );
+ }
+
+ public String fromGrantWithName(String token, String envId, String name, String childName) {
+ return null;
+ }
+
+ public String fromGrantWithUri(String token, String envId, String name, String childUri) {
+ return null;
+ }
+
+ public String fromELWithUri(String envId, String uriWithKey, String definitionKind, String definitionId) {
+ if (uriWithKey.contains(URI_KEY_SEPARATOR)) {
+ RefParser.UriAndKey uriAndKey = RefParser.parseUriAndKey(uriWithKey);
+ Ref ref = uriAndKey.asRef();
+ Spec spec = specRegistry.getFromUriAndKey(envId, uriWithKey);
+ if (spec == null && specLifecycleService.shouldDeployOnTheFly(ref)) {
+ spec = specLifecycleService.deployOnTheFly(envId, ref, false);
+ }
+ return grantAndGet(envId, definitionKind, definitionId, spec, ref, uriAndKey.key());
+ } else {
+ return resultToValue(new Result(Result.Type.ERROR, "uri must contain a key like such: /provider/uri:key"));
+ }
+ }
+
+ public String fromELWithName(String envId, String name, String definitionKind, String definitionId) {
+ Ref ref = new Ref(Ref.MainType.NAME, new Ref.Expression(name, false), null, null, name);
+ Spec spec = specRegistry.getFromName(envId, name);
+ return grantAndGet(envId, definitionKind, definitionId, spec, ref, spec.key());
+ }
+
+ private String grantAndGet(String envId, String definitionKind, String definitionId, Spec spec, Ref ref, String key) {
+ boolean granted = grantService.grant(
+ new DiscoveryContext(null, envId, ref, new DiscoveryLocation(new DiscoveryLocation.Definition(definitionKind, definitionId))),
+ spec
+ );
+ if (!granted) {
+ resultToValue(new Result(Result.Type.DENIED, "secret [%s] is denied in environment [%s]".formatted(spec.naturalId(), envId)));
+ }
+ return resultToValue(
+ toResult(
+ cache
+ .get(CacheKey.from(spec))
+ .orElse(
+ new Entry(
+ Entry.Type.EMPTY,
+ null,
+ "no value in cache for [%s] in environment [%s]".formatted(spec.naturalId(), envId)
+ )
+ ),
+ key
+ )
+ );
+ }
+
+ private Result toResult(Entry entry, String key) {
+ Result result;
+ switch (entry.type()) {
+ case VALUE -> {
+ Map secretMap = entry.value();
+ Secret secret = secretMap.get(key);
+ // TODO check expiration => then call ResolverService.resolveAsync(mount, retry=true)
+ // TODO need: spec(context ID => Spec)
+ // TODO need Secret to have expiration (not only map)
+ if (secret != null) {
+ result = new Result(Result.Type.VALUE, secret.asString());
+ } else {
+ result = new Result(Result.Type.KEY_NOT_FOUND, "key [%s] not found".formatted(key));
+ }
+ }
+ case EMPTY -> result = new Result(Result.Type.EMPTY, entry.error());
+ case NOT_FOUND -> result = new Result(Result.Type.NOT_FOUND, entry.error());
+ case ERROR -> result = new Result(Result.Type.ERROR, entry.error());
+ default -> result = null;
+ }
+ return result;
+ }
+
+ private String resultToValue(Result result) {
+ if (result != null) {
+ switch (result.type()) {
+ case VALUE -> {
+ return result.value();
+ }
+ case NOT_FOUND -> throw new SecretNotFoundException(result.value());
+ case KEY_NOT_FOUND -> throw new SecretKeyNotFoundException(result.value());
+ case EMPTY -> throw new SecretEmptyException(result.value());
+ case ERROR -> throw new SecretProviderException(result.value());
+ case DENIED -> throw new SecretAccessDeniedException(result.value());
+ }
+ }
+ return null;
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredEvaluationContext.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredEvaluationContext.java
new file mode 100644
index 000000000..77b76b4d5
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredEvaluationContext.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el.engine;
+
+import io.gravitee.el.spel.context.SecuredEvaluationContext;
+import java.util.Collections;
+import java.util.List;
+import org.springframework.expression.MethodResolver;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretSpelSecuredEvaluationContext extends SecuredEvaluationContext {
+
+ private static final List methodResolvers = Collections.singletonList(new SecretSpelSecuredMethodResolver());
+
+ public SecretSpelSecuredEvaluationContext() {
+ super();
+ }
+
+ @Override
+ public List getMethodResolvers() {
+ return methodResolvers;
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredMethodResolver.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredMethodResolver.java
new file mode 100644
index 000000000..91aacfc05
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredMethodResolver.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el.engine;
+
+import com.graviteesource.services.runtimesecrets.el.Service;
+import io.gravitee.el.spel.context.SecuredMethodResolver;
+import io.reactivex.rxjava3.annotations.NonNull;
+import java.lang.reflect.Method;
+import javax.annotation.Nonnull;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretSpelSecuredMethodResolver extends SecuredMethodResolver {
+
+ public SecretSpelSecuredMethodResolver() {
+ super();
+ }
+
+ @Nonnull
+ @Override
+ public @NonNull Method[] getMethods(@NonNull Class> type) {
+ if (type.equals(Service.class)) {
+ return Service.class.getMethods();
+ }
+ return super.getMethods(type);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateContext.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateContext.java
new file mode 100644
index 000000000..fe355b71f
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateContext.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el.engine;
+
+import io.gravitee.el.spel.context.SpelTemplateContext;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretSpelTemplateContext extends SpelTemplateContext {
+
+ public SecretSpelTemplateContext() {
+ super(new SecretSpelSecuredEvaluationContext());
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateEngine.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateEngine.java
new file mode 100644
index 000000000..61316d9a4
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateEngine.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el.engine;
+
+import io.gravitee.el.spel.SpelExpressionParser;
+import io.gravitee.el.spel.SpelTemplateEngine;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretSpelTemplateEngine extends SpelTemplateEngine {
+
+ public SecretSpelTemplateEngine(SpelExpressionParser spelExpressionParser) {
+ super(spelExpressionParser, new SecretSpelTemplateContext());
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java
new file mode 100644
index 000000000..2dabe34a5
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el.engine;
+
+import com.graviteesource.services.runtimesecrets.el.Service;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import io.gravitee.el.TemplateContext;
+import io.gravitee.el.TemplateVariableProvider;
+import io.gravitee.el.TemplateVariableScope;
+import io.gravitee.el.annotations.TemplateVariable;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RequiredArgsConstructor
+@TemplateVariable(scopes = { TemplateVariableScope.API, TemplateVariableScope.HEALTH_CHECK })
+public class SecretsTemplateVariableProvider implements TemplateVariableProvider {
+
+ private final Cache cache;
+ private final GrantService grantService;
+ private final SpecLifecycleService specLifecycleService;
+ private final SpecRegistry specRegistry;
+
+ @Override
+ public void provide(TemplateContext context) {
+ context.setVariable("secrets", new Service(cache, grantService, specLifecycleService, specRegistry));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java
new file mode 100644
index 000000000..9521f2f68
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretAccessDeniedException extends RuntimeSecretException {
+
+ public SecretAccessDeniedException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretEmptyException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretEmptyException.java
new file mode 100644
index 000000000..25a275606
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretEmptyException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretEmptyException extends RuntimeSecretException {
+
+ public SecretEmptyException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretKeyNotFoundException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretKeyNotFoundException.java
new file mode 100644
index 000000000..b7316ff95
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretKeyNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretKeyNotFoundException extends RuntimeSecretException {
+
+ public SecretKeyNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretNotFoundException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretNotFoundException.java
new file mode 100644
index 000000000..b85486dc9
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretNotFoundException extends RuntimeSecretException {
+
+ public SecretNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderException.java
new file mode 100644
index 000000000..dd9b5ade4
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretProviderException extends RuntimeSecretException {
+
+ public SecretProviderException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderNotFoundException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderNotFoundException.java
new file mode 100644
index 000000000..db0d99d6c
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretProviderNotFoundException extends RuntimeSecretException {
+
+ public SecretProviderNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretRefParsingException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretRefParsingException.java
new file mode 100644
index 000000000..d4de949c2
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretRefParsingException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretRefParsingException extends RuntimeSecretException {
+
+ public SecretRefParsingException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretSpecNotFoundException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretSpecNotFoundException.java
new file mode 100644
index 000000000..ad51b4fb5
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretSpecNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.errors;
+
+import io.gravitee.node.api.secrets.runtime.RuntimeSecretException;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretSpecNotFoundException extends RuntimeSecretException {
+
+ public SecretSpecNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java
new file mode 100644
index 000000000..e59021927
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.grant;
+
+import static com.graviteesource.services.runtimesecrets.config.Config.ON_THE_FLY_SPECS_ENABLED;
+import static io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation.PLUGIN_KIND;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.grant.Grant;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.spec.ACLs;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultGrantService implements GrantService {
+
+ private final GrantRegistry grantRegistry;
+ private final Config config;
+
+ @Override
+ public boolean grant(@Nonnull DiscoveryContext context, Spec spec) {
+ boolean granted = isGranted(context, spec);
+ if (granted && context.id() != null) {
+ grantRegistry.register(
+ context.id().toString(),
+ new Grant(CacheKey.from(spec), spec.key(), spec.valueKind(), spec.allowedFields())
+ );
+ }
+ return granted;
+ }
+
+ private boolean isGranted(DiscoveryContext context, Spec spec) {
+ if (spec == null) {
+ log.warn(
+ "no spec found for ref {} in envId [{}], {}={}",
+ context.ref().rawRef(),
+ context.envId(),
+ ON_THE_FLY_SPECS_ENABLED,
+ config.onTheFlySpecs().enabled()
+ );
+ return false;
+ }
+ if (spec.acls() == null) {
+ return checkSpec(context).test(spec);
+ }
+
+ return checkSpec(context).test(spec) && checkACLs(context).test(spec.acls());
+ }
+
+ @Override
+ public Optional getGrant(String contextId) {
+ return Optional.ofNullable(grantRegistry.get(contextId));
+ }
+
+ @Override
+ public void revoke(@Nonnull DiscoveryContext context) {
+ grantRegistry.unregister(context);
+ }
+
+ private Predicate checkSpec(DiscoveryContext context) {
+ Predicate envMatch = spec -> Objects.equals(context.envId(), spec.envId());
+
+ Predicate dynOrNoKey = spec -> spec.usesDynamicKey() || context.ref().secondaryType() == null;
+
+ Predicate keyMatch = spec ->
+ context.ref().secondaryType() == Ref.SecondaryType.KEY &&
+ context.ref().secondaryExpression().isLiteral() &&
+ spec.key().equals(context.ref().secondaryExpression().value());
+
+ return envMatch.and(keyMatch.or(dynOrNoKey));
+ }
+
+ private Predicate checkACLs(DiscoveryContext context) {
+ Predicate noDefKind = acls ->
+ acls.definitions() == null ||
+ acls.definitions().isEmpty() ||
+ acls.definitions().stream().allMatch(def -> def.kind() == null || def.kind().isEmpty());
+
+ Predicate defKindMatch = acls ->
+ acls.definitions().stream().anyMatch(defACLs -> defACLs.kind().contains(context.location().definition().kind()));
+
+ Predicate noDefId = acls ->
+ acls.definitions() == null ||
+ acls.definitions().isEmpty() ||
+ acls.definitions().stream().allMatch(def -> def.ids() == null || def.ids().isEmpty());
+
+ Predicate defIdMatch = acls ->
+ acls.definitions().stream().anyMatch(defACLs -> defACLs.ids().contains(context.location().definition().id()));
+
+ Predicate noPlugin = acls -> acls.plugins() == null || acls.plugins().isEmpty();
+
+ Predicate pluginMatch = acls ->
+ acls
+ .plugins()
+ .stream()
+ .anyMatch(pluginACL ->
+ Objects.equals(
+ pluginACL.id(),
+ Arrays
+ .stream(context.location().payloadLocations())
+ .filter(pl -> pl.kind().equals(PLUGIN_KIND))
+ .findFirst()
+ .orElse(PayloadLocation.NOWHERE)
+ .id()
+ )
+ );
+
+ return noDefKind.or(defKindMatch).and(noDefId.or(defIdMatch)).and(noPlugin.or(pluginMatch));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/GrantRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/GrantRegistry.java
new file mode 100644
index 000000000..b16018241
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/GrantRegistry.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.grant;
+
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.grant.Grant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class GrantRegistry {
+
+ private final Map grants = new ConcurrentHashMap<>();
+
+ public void register(String id, Grant grant) {
+ grants.put(id, grant);
+ }
+
+ public void unregister(DiscoveryContext... contexts) {
+ if (contexts != null) {
+ Arrays.stream(contexts).map(DiscoveryContext::id).map(UUID::toString).forEach(grants::remove);
+ }
+ }
+
+ public Grant get(String token) {
+ return grants.get(token);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultResolverService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultResolverService.java
new file mode 100644
index 000000000..465ce8d91
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultResolverService.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.providers;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import io.gravitee.common.utils.RxHelper;
+import io.gravitee.node.api.secrets.model.SecretMap;
+import io.gravitee.node.api.secrets.model.SecretMount;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.spec.Resolution;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.reactivex.rxjava3.annotations.NonNull;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.functions.Action;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class DefaultResolverService implements ResolverService {
+
+ private final SecretProviderRegistry secretProviderRegistry;
+ private final Cache cache;
+ private final Config config;
+
+ @Override
+ public Single resolve(String envId, SecretMount secretMount) {
+ return resolve(envId, secretMount, new Resolution(Resolution.Type.ONCE, null));
+ }
+
+ public Single resolve(String envId, SecretMount secretMount, Resolution resolution) {
+ return secretProviderRegistry
+ .get(envId, secretMount.provider())
+ .flatMapMaybe(secretProvider -> secretProvider.resolve(secretMount))
+ .map(secretMap -> this.applyExpiration(secretMap, resolution))
+ .map(secretMap -> new Entry(Entry.Type.VALUE, secretMap.asMap(), null))
+ .defaultIfEmpty(new Entry(Entry.Type.NOT_FOUND, null, null))
+ .onErrorResumeNext(t -> Single.just(new Entry(Entry.Type.ERROR, null, t.getMessage())));
+ }
+
+ @Override
+ public void resolveAsync(Spec spec, Duration delayBeforeResolve, boolean retryOnError, @NonNull Action postResolution) {
+ SecretURL secretURL = spec.toSecretURL();
+
+ this.toSecretMount(spec.envId(), secretURL)
+ .delay(delayBeforeResolve.toMillis(), TimeUnit.MILLISECONDS)
+ .retryWhen(
+ RxHelper.retryExponentialBackoff(
+ config.retry().delay(),
+ config.retry().maxDelay(),
+ config.retry().unit(),
+ config.retry().backoffFactor(),
+ err -> retryOnError && config.retry().enabled()
+ )
+ )
+ .doOnSuccess(mount -> log.info("Resolving secret: {}", mount))
+ .flatMap(mount -> this.resolve(spec.envId(), mount))
+ .doOnError(err -> log.error("Async resolution failed", err))
+ .doFinally(postResolution)
+ .subscribe(entry -> cache.put(CacheKey.from(spec), entry));
+ }
+
+ private SecretMap applyExpiration(SecretMap secretMap, Resolution resolution) {
+ if (resolution.type() == Resolution.Type.TTL) {
+ // TODO move to Secret object
+ secretMap.withExpireAt(Instant.now().plus(resolution.duration()));
+ }
+ return secretMap;
+ }
+
+ @Override
+ public Single toSecretMount(String envId, SecretURL secretURL) {
+ return secretProviderRegistry.get(envId, secretURL.provider()).map(provider -> provider.fromURL(secretURL));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/SecretProviderRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/SecretProviderRegistry.java
new file mode 100644
index 000000000..5519ec2a9
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/SecretProviderRegistry.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.providers;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.graviteesource.services.runtimesecrets.errors.SecretProviderNotFoundException;
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.secrets.service.AbstractSecretProviderDispatcher;
+import io.reactivex.rxjava3.core.Maybe;
+import io.reactivex.rxjava3.core.Single;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretProviderRegistry {
+
+ private final Multimap perEnv = MultimapBuilder.hashKeys().arrayListValues().build();
+ private final Map allEnvs = new ConcurrentHashMap<>();
+
+ public void register(String id, SecretProvider provider, String envId) {
+ if (envId == null || envId.isEmpty()) {
+ allEnvs.put(id, provider);
+ } else {
+ synchronized (perEnv) {
+ perEnv.put(envId, new SecretProviderEntry(id, provider));
+ }
+ }
+ }
+
+ /**
+ *
+ * @param envId environment ID
+ * @param id is of the provider
+ * @return a secret provider
+ * @throws SecretProviderNotFoundException if the provider is not found
+ */
+ public Single get(String envId, String id) {
+ return Maybe
+ .defer(() -> {
+ Collection perEnvProviders;
+ synchronized (perEnv) {
+ perEnvProviders = perEnv.get(envId);
+ }
+
+ return Maybe.fromOptional(
+ perEnvProviders
+ .stream()
+ .filter(entry -> entry.id().equals(id))
+ .map(SecretProviderEntry::provider)
+ .findFirst()
+ .or(() -> Optional.ofNullable(allEnvs.get(id)))
+ );
+ })
+ .switchIfEmpty(Single.just(new AbstractSecretProviderDispatcher.ErrorSecretProvider()));
+ }
+
+ record SecretProviderEntry(String id, SecretProvider provider) {}
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/config/FromConfigurationSecretProviderDeployer.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/config/FromConfigurationSecretProviderDeployer.java
new file mode 100644
index 000000000..b1d30f817
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/config/FromConfigurationSecretProviderDeployer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.providers.config;
+
+import static com.graviteesource.services.runtimesecrets.config.Config.CONFIG_PREFIX;
+
+import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry;
+import io.gravitee.common.util.EnvironmentUtils;
+import io.gravitee.node.api.secrets.SecretManagerConfiguration;
+import io.gravitee.node.api.secrets.SecretProviderFactory;
+import io.gravitee.node.api.secrets.errors.SecretManagerConfigurationException;
+import io.gravitee.node.api.secrets.errors.SecretProviderNotFoundException;
+import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer;
+import io.gravitee.node.api.secrets.util.ConfigHelper;
+import io.gravitee.node.secrets.plugins.SecretProviderPlugin;
+import io.gravitee.node.secrets.plugins.SecretProviderPluginManager;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+
+@RequiredArgsConstructor
+@Slf4j
+public class FromConfigurationSecretProviderDeployer implements SecretProviderDeployer {
+
+ private final ConfigurableEnvironment environment;
+ private final SecretProviderRegistry registry;
+ private final SecretProviderPluginManager secretProviderPluginManager;
+
+ public void init() {
+ log.info("loading runtime secret providers from configuration");
+ Map allProperties = EnvironmentUtils.getAllProperties(environment);
+ Map apiSecrets = ConfigHelper.removePrefix(allProperties, CONFIG_PREFIX);
+ int i = 0;
+ String provider = provider(i);
+ while (apiSecrets.containsKey(provider + ".plugin")) {
+ Map providerConfig = ConfigHelper.removePrefix(apiSecrets, provider);
+ if (!ConfigHelper.getProperty(providerConfig, "configuration.enabled", Boolean.class, true)) {
+ return;
+ }
+ String plugin = ConfigHelper.getProperty(providerConfig, "plugin", String.class);
+ String id = ConfigHelper.getProperty(providerConfig, "id", String.class, plugin);
+ int e = 0;
+ String environment = environment(e);
+ while (providerConfig.containsKey(environment)) {
+ String envId = providerConfig.get(environment).toString();
+ deploy(plugin, ConfigHelper.removePrefix(providerConfig, "configuration"), id, envId);
+ environment = environment(++e);
+ }
+ // no env
+ if (e == 0) {
+ deploy(plugin, ConfigHelper.removePrefix(providerConfig, "configuration"), id, null);
+ }
+ provider = provider(++i);
+ }
+ }
+
+ @Override
+ public void deploy(String pluginId, Map configurationProperties, String providerId, String envId) {
+ try {
+ log.info("Deploying secret provider [{}] of type [{}] for environment [{}]...", providerId, pluginId, formatEnv(envId));
+ final SecretProviderPlugin, ?> secretProviderPlugin = secretProviderPluginManager.get(pluginId);
+ final Class extends SecretManagerConfiguration> configurationClass = secretProviderPlugin.configuration();
+ final SecretProviderFactory factory = secretProviderPluginManager.getFactoryById(pluginId);
+ if (configurationClass != null && factory != null) {
+ // read the config using the plugin class loader
+ SecretManagerConfiguration config;
+ Class> configurationClass1 = factory.getClass().getClassLoader().loadClass(configurationClass.getName());
+ try {
+ @SuppressWarnings("unchecked")
+ Constructor constructor = (Constructor) configurationClass1.getDeclaredConstructor(
+ Map.class
+ );
+ config = constructor.newInstance(configurationProperties);
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new SecretManagerConfigurationException(
+ "Could not create configuration class for secret manager: %s".formatted(providerId),
+ e
+ );
+ }
+ log.info("Secret provider [{}] of type [{}] for environment [{}]: DEPLOYED", providerId, pluginId, formatEnv(envId));
+ this.registry.register(providerId, factory.create(config).start(), envId);
+ } else {
+ log.info("Secret provider [{}] of type [{}] for environment [{}]: FAILED", providerId, pluginId, formatEnv(envId));
+ throw new SecretProviderNotFoundException("Cannot find secret provider [%s] plugin".formatted(pluginId));
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("cannot load plugin %s properly: ".formatted(pluginId));
+ }
+ }
+
+ private static String formatEnv(String envId) {
+ return envId == null ? "*" : envId;
+ }
+
+ private static String provider(int i) {
+ return "providers[%d]".formatted(i);
+ }
+
+ private static String environment(int e) {
+ return "environments[%d]".formatted(e);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java
new file mode 100644
index 000000000..457c0d0b5
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.renewal;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.errors.SecretProviderException;
+import com.graviteesource.services.runtimesecrets.spec.SpecUpdate;
+import io.gravitee.node.api.secrets.model.Secret;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.spec.Resolution;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.disposables.Disposable;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class RenewalService {
+
+ private final ResolverService resolverService;
+ private final Cache cache;
+ private final Config config;
+ private Disposable poller;
+ private final Map specsToRenew = new ConcurrentHashMap<>();
+
+ public void onSpec(Spec spec) {
+ onSpec(new SpecUpdate(null, spec));
+ }
+
+ public void onSpec(SpecUpdate specUpdate) {
+ Spec oldSpec = specUpdate.oldSpec();
+ if (oldSpec != null) {
+ specsToRenew.remove(oldSpec);
+ }
+ if (specUpdate.newSpec().hasResolutionType(Resolution.Type.POLL)) {
+ setupNextCheck(specUpdate.newSpec());
+ }
+ }
+
+ public void onDelete(Spec spec) {
+ if (spec != null) {
+ specsToRenew.remove(spec);
+ }
+ }
+
+ public void start() {
+ if (config.renewal().enabled()) {
+ doStart();
+ }
+ }
+
+ private void doStart() {
+ poller =
+ Flowable
+ .generate(
+ Instant::now,
+ (state, emitter) -> {
+ emitter.onNext(state);
+ return Instant.now();
+ }
+ )
+ .delay(config.renewal().duration().toMillis(), TimeUnit.MILLISECONDS)
+ .rebatchRequests(1)
+ .concatMap(now ->
+ Flowable
+ .fromIterable(specsToRenew.entrySet())
+ .filter(specAndTime -> now.isAfter(specAndTime.getValue()))
+ .map(Map.Entry::getKey)
+ .doOnNext(this::setupNextCheck)
+ .groupBy(CacheKey::from)
+ .flatMapSingle(group -> {
+ CacheKey cacheKey = group.getKey();
+ return resolverService
+ .toSecretMount(cacheKey.envId(), SecretURL.from(cacheKey.uri(), false))
+ .flatMap(secretMount -> resolverService.resolve(cacheKey.envId(), secretMount))
+ .flatMap(entry -> {
+ if (entry.type() == Entry.Type.VALUE) {
+ return group.collect(
+ HashMap::new,
+ (map, spec) -> {
+ String key = spec.key();
+ map.put(key, entry.value().get(key));
+ }
+ );
+ } else if (entry.type() == Entry.Type.EMPTY) {
+ return Single.just(Map.of());
+ }
+ return Single.error(new SecretProviderException(entry.error()));
+ })
+ .doOnError(err -> log.warn("Renewal failed for cache-key [{}]: {}", cacheKey, err.getMessage()))
+ .doOnSuccess(map -> cache.putPartial(cacheKey, map));
+ })
+ )
+ .subscribe();
+ }
+
+ public void stop() {
+ if (poller != null) {
+ poller.dispose();
+ }
+ }
+
+ private void setupNextCheck(Spec spec) {
+ specsToRenew.put(spec, Instant.now().plus(spec.resolution().duration()));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java
new file mode 100644
index 000000000..dbb236221
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spec;
+
+import static java.util.function.Predicate.not;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import io.gravitee.node.api.secrets.model.SecretMount;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.runtime.discovery.ContextRegistry;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.reactivex.rxjava3.functions.Action;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.function.Predicate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class DefaultSpecLifecycleService implements SpecLifecycleService {
+
+ private final SpecRegistry specRegistry;
+ private final ContextRegistry contextRegistry;
+ private final Cache cache;
+ private final ResolverService resolverService;
+ private final GrantService grantService;
+ private final RenewalService renewalService;
+ private final Config config;
+
+ @Override
+ public boolean shouldDeployOnTheFly(Ref ref) {
+ return (ref.mainType() == Ref.MainType.URI && ref.mainExpression().isLiteral() && config.onTheFlySpecs().enabled());
+ }
+
+ @Override
+ public Spec deployOnTheFly(String envId, Ref ref, boolean retryOnError) {
+ Spec runtimeSpec = ref.asOnTheFlySpec(envId);
+ specRegistry.register(runtimeSpec);
+ cache.computeIfAbsent(
+ CacheKey.from(runtimeSpec),
+ () -> {
+ SecretURL secretURL = runtimeSpec.toSecretURL();
+ return resolverService
+ .toSecretMount(envId, secretURL)
+ .doOnSuccess(mount -> log.info("Resolving secret (On the fly): {}", mount))
+ .map(SecretMount::withoutRetries)
+ .flatMap(mount ->
+ resolverService
+ .resolve(envId, mount)
+ .doOnSuccess(entry -> {
+ if (entry.type() == Entry.Type.ERROR) {
+ resolverService.resolveAsync(
+ runtimeSpec,
+ config.onTheFlySpecs().onErrorRetryAfter(),
+ retryOnError,
+ () -> {}
+ );
+ }
+ })
+ )
+ .blockingGet();
+ }
+ );
+
+ return runtimeSpec;
+ }
+
+ @Override
+ public void deploy(Spec spec) {
+ Spec currentSpec = specRegistry.fromSpec(spec);
+ log.info("Deploying Secret Spec: {}", spec);
+ Action afterResolve = () -> specRegistry.register(spec);
+ boolean shouldResolve = true;
+ if (currentSpec != null) {
+ SpecUpdate update = new SpecUpdate(currentSpec, spec);
+ if (isUriAndKeyChanged(update)) {
+ afterResolve =
+ () -> {
+ renewGrant(update);
+ specRegistry.replace(update);
+ if (!Objects.equals(CacheKey.from(update.oldSpec()), CacheKey.from(update.newSpec()))) {
+ cache.evict(CacheKey.from(update.oldSpec()));
+ }
+ };
+ } else if (isACLsChange(update)) {
+ renewGrant(update);
+ specRegistry.replace(update);
+ shouldResolve = false;
+ }
+ renewalService.onSpec(update);
+ } else {
+ renewalService.onSpec(spec);
+ contextRegistry.findBySpec(spec).forEach(context -> grantService.grant(context, spec));
+ }
+
+ if (shouldResolve) {
+ resolverService.resolveAsync(spec, Duration.ZERO, true, afterResolve);
+ }
+ }
+
+ private void renewGrant(SpecUpdate update) {
+ contextRegistry
+ .findBySpec(update.oldSpec())
+ .forEach(context -> {
+ boolean grant = grantService.grant(context, update.newSpec());
+ if (!grant) {
+ grantService.revoke(context);
+ }
+ });
+ }
+
+ private static boolean isACLsChange(SpecUpdate specUpdate) {
+ Predicate sameACLs = update -> Objects.equals(update.oldSpec().acls(), update.newSpec().acls());
+ Predicate sameKind = update -> update.oldSpec().valueKind() == update.newSpec().valueKind();
+ return not(sameACLs).or(not(sameKind)).test(specUpdate);
+ }
+
+ private boolean isUriAndKeyChanged(SpecUpdate update) {
+ return !Objects.equals(update.oldSpec().uriAndKey(), update.newSpec().uriAndKey());
+ }
+
+ @Override
+ public void undeploy(Spec spec) {
+ contextRegistry.findBySpec(spec).forEach(grantService::revoke);
+ specRegistry.unregister(spec);
+ cache.evict(CacheKey.from(spec));
+ renewalService.onDelete(spec);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecRegistry.java
new file mode 100644
index 000000000..8851b1211
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecRegistry.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spec;
+
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Slf4j
+public class SpecRegistry {
+
+ private final Map registries = new ConcurrentHashMap<>();
+
+ public void register(Spec spec) {
+ registry(spec.envId()).register(spec);
+ }
+
+ public void unregister(Spec spec) {
+ registry(spec.envId()).unregister(spec);
+ }
+
+ public void replace(SpecUpdate update) {
+ String envId = update.oldSpec().envId();
+ synchronized (registry(envId)) {
+ registry(envId).unregister(update.oldSpec());
+ registry(envId).register(update.newSpec());
+ }
+ }
+
+ public Spec getFromName(String envId, String name) {
+ return registry(envId).getFromName(name);
+ }
+
+ public Spec getFromUriAndKey(String envId, String uriAndKey) {
+ return registry(envId).getFromUriAndKey(uriAndKey);
+ }
+
+ public Spec fromSpec(Spec query) {
+ return registry(query.envId()).fromSpec(query);
+ }
+
+ public Spec fromRef(String envId, Ref query) {
+ return registry(envId).fromRef(query);
+ }
+
+ private Registry registry(String envId) {
+ return registries.computeIfAbsent(envId, ignore -> new Registry());
+ }
+
+ private static class Registry {
+
+ private final Map byName = new ConcurrentHashMap<>();
+ private final Map byUriAndKey = new ConcurrentHashMap<>();
+ private final Map byID = new ConcurrentHashMap<>();
+
+ void register(Spec spec) {
+ if (spec.id() != null) {
+ byID.put(spec.id(), spec);
+ }
+ if (spec.name() != null) {
+ byName.put(spec.name(), spec);
+ }
+ if (spec.uri() != null && spec.key() != null) {
+ byUriAndKey.put(spec.uriAndKey(), spec);
+ }
+ }
+
+ void unregister(Spec spec) {
+ if (spec.id() != null) {
+ byID.remove(spec.id());
+ }
+ if (spec.uri() != null && spec.key() != null) {
+ byUriAndKey.remove(spec.uriAndKey(), spec);
+ }
+ if (spec.name() != null) {
+ byName.remove(spec.name());
+ }
+ }
+
+ Spec getFromName(String name) {
+ return byName.get(name);
+ }
+
+ Spec getFromUriAndKey(String uriAndKey) {
+ return byUriAndKey.get(uriAndKey);
+ }
+
+ Spec fromRef(Ref query) {
+ if (query.mainType() == Ref.MainType.NAME) {
+ return byName.get(query.mainExpression().value());
+ }
+ if (query.mainType() == Ref.MainType.URI) {
+ if (query.secondaryType() == Ref.SecondaryType.KEY) {
+ return byUriAndKey.get(query.uriAndKey());
+ }
+ }
+ return null;
+ }
+
+ Spec fromSpec(Spec query) {
+ Spec result = null;
+ if (query.id() != null) {
+ result = byID.get(query.id());
+ }
+ if (result == null && query.name() != null) {
+ result = byName.get(query.name());
+ }
+ if (result == null && query.uri() != null && query.key() != null) {
+ result = byUriAndKey.get(query.uriAndKey());
+ }
+ return result;
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecUpdate.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecUpdate.java
new file mode 100644
index 000000000..cc2bcb258
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecUpdate.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spec;
+
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record SpecUpdate(Spec oldSpec, Spec newSpec) {}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/RuntimeSecretsBeanFactory.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/RuntimeSecretsBeanFactory.java
new file mode 100644
index 000000000..00e64eb03
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/RuntimeSecretsBeanFactory.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spring;
+
+import static com.graviteesource.services.runtimesecrets.config.Config.*;
+
+import com.graviteesource.services.runtimesecrets.Processor;
+import com.graviteesource.services.runtimesecrets.RuntimeSecretsService;
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.config.OnTheFlySpecs;
+import com.graviteesource.services.runtimesecrets.config.Renewal;
+import com.graviteesource.services.runtimesecrets.config.Retry;
+import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry;
+import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry;
+import com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider;
+import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService;
+import com.graviteesource.services.runtimesecrets.grant.GrantRegistry;
+import com.graviteesource.services.runtimesecrets.providers.DefaultResolverService;
+import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry;
+import com.graviteesource.services.runtimesecrets.providers.config.FromConfigurationSecretProviderDeployer;
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import com.graviteesource.services.runtimesecrets.spec.DefaultSpecLifecycleService;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache;
+import io.gravitee.node.api.secrets.runtime.discovery.ContextRegistry;
+import io.gravitee.node.api.secrets.runtime.discovery.DefinitionBrowser;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.secrets.plugins.SecretProviderPluginManager;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.*;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.Environment;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Configuration
+public class RuntimeSecretsBeanFactory {
+
+ @Bean
+ Config config(
+ @Value("${" + ON_THE_FLY_SPECS_ENABLED + ":true}") boolean onTheFlySpecsEnabled,
+ @Value("${" + ON_THE_FLY_SPECS_ON_ERROR_RETRY_AFTER_DELAY + ":500}") long onTheFlySpecsOnErrorRetryAfterDelay,
+ @Value("${" + ON_THE_FLY_SPECS_ON_ERROR_RETRY_AFTER_UNIT + ":MILLISECONDS}") TimeUnit onTheFlySpecsOnErrorRetryAfterUnit,
+ @Value("${" + RENEWAL_ENABLED + ":true}") boolean renewalEnabled,
+ @Value("${" + RENEWAL_CHECK_DELAY + ":15}") long renewalCheckDelay,
+ @Value("${" + RENEWAL_CHECK_UNIT + ":MINUTES}") TimeUnit renewalCheckUnit,
+ @Value("${" + RETRY_ON_ERROR_ENABLED + ":false}") boolean retryOnErrorEnabled,
+ @Value("${" + RETRY_ON_ERROR_DELAY + ":2}") long retryOnErrorDelay,
+ @Value("${" + RETRY_ON_ERROR_UNIT + ":SECONDS}") TimeUnit retryOnErrorUnit,
+ @Value("${" + RETRY_ON_ERROR_BACKOFF_FACTOR + ":1.5}") float factor,
+ @Value("${" + RETRY_ON_ERROR_BACKOFF_MAX_DELAY + ":60}") int maxDelay
+ ) {
+ return new Config(
+ new OnTheFlySpecs(
+ onTheFlySpecsEnabled,
+ Duration.of(onTheFlySpecsOnErrorRetryAfterDelay, onTheFlySpecsOnErrorRetryAfterUnit.toChronoUnit())
+ ),
+ new Retry(retryOnErrorEnabled, retryOnErrorDelay, retryOnErrorUnit, factor, maxDelay),
+ new Renewal(renewalEnabled, Duration.of(renewalCheckDelay, renewalCheckUnit.toChronoUnit()))
+ );
+ }
+
+ @Bean
+ RuntimeSecretsService runtimeSecretsService(
+ Processor processor,
+ SpecLifecycleService specLifecycleService,
+ SpecRegistry specRegistry,
+ SecretProviderDeployer secretProviderDeployer,
+ RenewalService renewalService,
+ Environment environment
+ ) {
+ return new RuntimeSecretsService(
+ processor,
+ specLifecycleService,
+ specRegistry,
+ secretProviderDeployer,
+ renewalService,
+ environment
+ );
+ }
+
+ @Bean
+ Processor processor(
+ DefinitionBrowserRegistry definitionBrowserRegistry,
+ ContextRegistry contextRegistry,
+ SpecRegistry specRegistry,
+ SpecLifecycleService specLifecycleService,
+ GrantService grantService
+ ) {
+ return new Processor(definitionBrowserRegistry, contextRegistry, specRegistry, grantService, specLifecycleService);
+ }
+
+ @Bean
+ ContextRegistry contextRegistry() {
+ return new DefaultContextRegistry();
+ }
+
+ @Bean
+ DefinitionBrowserRegistry definitionBrowserRegistry(List browsers) {
+ return new DefinitionBrowserRegistry(browsers);
+ }
+
+ @Bean
+ RenewalService renewalService(ResolverService resolverService, Cache cache, Config config) {
+ return new RenewalService(resolverService, cache, config);
+ }
+
+ @Bean
+ SpecLifecycleService specLifecycleService(
+ SpecRegistry specRegistry,
+ ContextRegistry contextRegistry,
+ Cache cache,
+ ResolverService resolverService,
+ GrantService grantService,
+ Config config,
+ RenewalService renewalService
+ ) {
+ return new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, renewalService, config);
+ }
+
+ @Bean
+ Cache secretCache() {
+ return new SimpleOffHeapCache();
+ }
+
+ @Bean
+ GrantService grantService(Config config) {
+ return new DefaultGrantService(new GrantRegistry(), config);
+ }
+
+ @Bean
+ SpecRegistry envAwareSpecRegistry() {
+ return new SpecRegistry();
+ }
+
+ @Bean
+ SecretProviderRegistry secretProviderRegistry() {
+ return new SecretProviderRegistry();
+ }
+
+ @Bean
+ @Conditional(AllowGraviteeYmlProviders.class)
+ SecretProviderDeployer runtimeSecretProviderDeployer(
+ ConfigurableEnvironment environment,
+ SecretProviderRegistry secretProviderRegistry,
+ SecretProviderPluginManager pluginManager
+ ) {
+ return new FromConfigurationSecretProviderDeployer(environment, secretProviderRegistry, pluginManager);
+ }
+
+ @Bean
+ @Conditional({ AllowGraviteeYmlProviders.class })
+ ResolverService resolverService(SecretProviderRegistry secretProviderRegistry, Cache cache, Config config) {
+ return new DefaultResolverService(secretProviderRegistry, cache, config);
+ }
+
+ @Bean
+ @Conditional({ DenyConfigProviders.class })
+ ResolverService runtimeSecretResolver() {
+ return null;
+ }
+
+ @Bean
+ SecretsTemplateVariableProvider secretsTemplateVariableProvider(
+ Cache cache,
+ GrantService grantService,
+ SpecLifecycleService specLifecycleService,
+ SpecRegistry specRegistry
+ ) {
+ return new SecretsTemplateVariableProvider(cache, grantService, specLifecycleService, specRegistry);
+ }
+
+ private static final Predicate ALLOW_PROVIDERS_FROM_CONFIG = context ->
+ context.getEnvironment().getProperty(API_SECRETS_ALLOW_PROVIDERS_FROM_CONFIGURATION, Boolean.class, true);
+
+ static class AllowGraviteeYmlProviders implements Condition {
+
+ @Override
+ public boolean matches(ConditionContext context, AnnotatedTypeMetadata ignore) {
+ return ALLOW_PROVIDERS_FROM_CONFIG.test(context);
+ }
+ }
+
+ static class DenyConfigProviders implements Condition {
+
+ @Override
+ public boolean matches(ConditionContext context, AnnotatedTypeMetadata ignore) {
+ return ALLOW_PROVIDERS_FROM_CONFIG.negate().test(context);
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/SecretTemplateEngineFactory.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/SecretTemplateEngineFactory.java
new file mode 100644
index 000000000..7dea8e20f
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/SecretTemplateEngineFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spring;
+
+import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine;
+import io.gravitee.el.TemplateEngine;
+import io.gravitee.el.TemplateEngineFactory;
+import io.gravitee.el.spel.SpelExpressionParser;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SecretTemplateEngineFactory implements TemplateEngineFactory {
+
+ @Override
+ public TemplateEngine templateEngine() {
+ return new SecretSpelTemplateEngine(new SpelExpressionParser());
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCache.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCache.java
new file mode 100644
index 000000000..2cbe31555
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCache.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.storage;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import io.gravitee.node.api.secrets.model.Secret;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SimpleOffHeapCache implements Cache {
+
+ private final Kryo kryo;
+
+ public SimpleOffHeapCache() {
+ this.kryo = new Kryo();
+ kryo.register(Secret.class);
+ kryo.register(Entry.class);
+ kryo.register(Entry.Type.class);
+ kryo.register(HashMap.class);
+ }
+
+ private final ConcurrentMap data = new ConcurrentHashMap<>();
+
+ @Override
+ public void put(CacheKey cacheKey, Entry value) {
+ var bytes = serialize(value);
+ final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
+ data.put(cacheKey, byteBuffer);
+ byteBuffer.put(bytes);
+ }
+
+ @Override
+ public void putPartial(CacheKey cacheKey, Map partial) {
+ Optional entryOpt = this.get(cacheKey);
+ Entry entry;
+ if (entryOpt.isPresent()) {
+ entry = entryOpt.get();
+ if (entry.type() == Entry.Type.VALUE) {
+ Map secretMap = entry.value();
+ secretMap.putAll(partial);
+ entry = new Entry(Entry.Type.VALUE, secretMap, null);
+ } else {
+ entry = new Entry(Entry.Type.VALUE, partial, null);
+ }
+ } else {
+ entry = new Entry(Entry.Type.VALUE, partial, null);
+ }
+ this.put(cacheKey, entry);
+ }
+
+ @Override
+ public Optional get(CacheKey cacheKey) {
+ ByteBuffer byteBuffer = data.get(cacheKey);
+ if (byteBuffer != null) {
+ byte[] buf = new byte[byteBuffer.limit()];
+ byteBuffer.position(0);
+ byteBuffer.get(buf, 0, buf.length);
+ return Optional.of(deserialize(buf));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void computeIfAbsent(CacheKey cacheKey, Supplier supplier) {
+ data.computeIfAbsent(
+ cacheKey,
+ key -> {
+ Entry value = supplier.get();
+ byte[] stringAsBytes = serialize(value);
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(stringAsBytes.length);
+ byteBuffer.put(stringAsBytes);
+ return byteBuffer;
+ }
+ );
+ }
+
+ @Override
+ public void evict(CacheKey cacheKey) {
+ data.remove(cacheKey);
+ }
+
+ public byte[] serialize(Entry value) {
+ try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); Output out = new Output(bytes)) {
+ this.kryo.writeObject(out, value);
+ return out.toBytes();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public Entry deserialize(byte[] bytes) {
+ try (Input in = new Input(bytes)) {
+ return this.kryo.readObject(in, Entry.class);
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateEngineFactory b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateEngineFactory
new file mode 100644
index 000000000..3291a678f
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateEngineFactory
@@ -0,0 +1 @@
+com.graviteesource.services.runtimesecrets.spring.SecretTemplateEngineFactory
\ No newline at end of file
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateVariableProvider b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateVariableProvider
new file mode 100644
index 000000000..ec925d2f3
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateVariableProvider
@@ -0,0 +1 @@
+com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider
\ No newline at end of file
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/spring.factories b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..7a1d11184
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+io.gravitee.el.TemplateVariableProvider=\
+ com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider
\ No newline at end of file
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java
new file mode 100644
index 000000000..2e10ee6de
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.awaitility.Awaitility.await;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.config.OnTheFlySpecs;
+import com.graviteesource.services.runtimesecrets.config.Renewal;
+import com.graviteesource.services.runtimesecrets.config.Retry;
+import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry;
+import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry;
+import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine;
+import com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider;
+import com.graviteesource.services.runtimesecrets.errors.SecretAccessDeniedException;
+import com.graviteesource.services.runtimesecrets.errors.SecretProviderException;
+import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService;
+import com.graviteesource.services.runtimesecrets.grant.GrantRegistry;
+import com.graviteesource.services.runtimesecrets.providers.DefaultResolverService;
+import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry;
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import com.graviteesource.services.runtimesecrets.spec.DefaultSpecLifecycleService;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache;
+import io.gravitee.el.spel.SpelExpressionParser;
+import io.gravitee.el.spel.SpelTemplateEngine;
+import io.gravitee.node.api.secrets.runtime.discovery.*;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.spec.ACLs;
+import io.gravitee.node.api.secrets.runtime.spec.Resolution;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.gravitee.node.secrets.plugin.mock.MockSecretProvider;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.awaitility.core.ConditionFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.security.util.InMemoryResource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class ProcessorTest {
+
+ public static final String FOO_ENV_ID = "foo";
+ public static final String BAR_ENV_ID = "bar";
+ InMemoryResource providerAllEnv = new InMemoryResource(
+ """
+ secrets:
+ mySecret:
+ redisPassword: "fighters"
+ ldapPassword: "dog"
+ secondSecret:
+ ldapPassword: "yeah!"
+ flaky:
+ password: iamflaky
+ rotating:
+ password: secret1
+ errors:
+ - secret: flaky
+ message: huge error!!!
+ repeat: 1
+ - secret: error
+ message: I am not in the mood
+ renewals:
+ - secret: rotating
+ revisions:
+ - data:
+ password: secret2
+ - data:
+ password: secret3
+ """
+ );
+ InMemoryResource providerBarEnv = new InMemoryResource(
+ """
+ secrets:
+ mySecret:
+ redisPassword: "tender"
+ ldapPassword: "regular"
+
+ """
+ );
+ private SpecLifecycleService specLifeCycleService;
+ private Cache cache;
+ private SpelTemplateEngine spelTemplateEngine;
+ private Processor cut;
+ private RenewalService renewalService;
+
+ @BeforeEach
+ void before() {
+ SecretProviderRegistry secretProviderRegistry = new SecretProviderRegistry();
+
+ final YamlPropertiesFactoryBean allEnvSPConfig = new YamlPropertiesFactoryBean();
+ allEnvSPConfig.setResources(providerAllEnv);
+ secretProviderRegistry.register(
+ "mock",
+ new MockSecretProvider(new MockSecretProviderConfiguration((Map) new LinkedHashMap<>(allEnvSPConfig.getObject()))),
+ null
+ );
+
+ final YamlPropertiesFactoryBean barEnvSPConfig = new YamlPropertiesFactoryBean();
+ barEnvSPConfig.setResources(providerBarEnv);
+ secretProviderRegistry.register(
+ "mock-bar",
+ new MockSecretProvider(new MockSecretProviderConfiguration((Map) new LinkedHashMap<>(barEnvSPConfig.getObject()))),
+ BAR_ENV_ID
+ );
+
+ cache = new SimpleOffHeapCache();
+ Config config = new Config(
+ new OnTheFlySpecs(true, Duration.ofMillis(200)),
+ Retry.none(),
+ new Renewal(true, Duration.ofMillis(200))
+ );
+ GrantService grantService = new DefaultGrantService(new GrantRegistry(), config);
+ SpecRegistry specRegistry = new SpecRegistry();
+ ContextRegistry contextRegistry = new DefaultContextRegistry();
+ ResolverService resolverService = new DefaultResolverService(secretProviderRegistry, cache, config);
+
+ renewalService = new RenewalService(resolverService, cache, config);
+ specLifeCycleService =
+ new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, renewalService, config);
+ SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider(
+ cache,
+ grantService,
+ specLifeCycleService,
+ specRegistry
+ );
+ spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser());
+ // set up EL variables
+ secretsTemplateVariableProvider.provide(spelTemplateEngine.getTemplateContext());
+ spelTemplateEngine.getTemplateContext().setVariable("uris", Map.of("redis", "/mock/mySecret:redisPassword"));
+
+ DefinitionBrowserRegistry browserRegistry = new DefinitionBrowserRegistry(List.of(new TestDefinitionBrowser()));
+ cut = new Processor(browserRegistry, contextRegistry, specRegistry, grantService, specLifeCycleService);
+ }
+
+ @Test
+ void should_discover_and_resolve_secret_on_the_fly() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ }
+
+ @Test
+ void should_discover_and_resolve_secret_on_the_fly_with_mixed_string() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "Redis password is: <>!", "");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("Redis password is: fighters!");
+ }
+
+ @Test
+ void should_discover_and_resolve_secret_on_the_fly_from_el() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "Redis password is: << uri {#uris['redis']} >>!", "");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("Redis password is: fighters!");
+ }
+
+ @Test
+ void should_discover_and_get_secret() {
+ final String name = "redis-password";
+ Spec spec = new Spec(null, name, "/mock/mySecret", "redisPassword", null, false, false, null, null, FOO_ENV_ID);
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<<" + name + ">>", "");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ }
+
+ @Test
+ void should_get_a_different_secret_in_two_different_env() {
+ final String name = "redis-password";
+ Spec fooSpec = new Spec(null, name, "/mock/mySecret", "redisPassword", null, false, false, null, null, FOO_ENV_ID);
+ Spec barSpec = new Spec(null, name, "/mock-bar/mySecret", "redisPassword", null, false, false, null, null, BAR_ENV_ID);
+ specLifeCycleService.deploy(fooSpec);
+ specLifeCycleService.deploy(barSpec);
+
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(fooSpec))).isPresent());
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(barSpec))).isPresent());
+
+ FakeDefinition fooDefinition = new FakeDefinition("123", "<<" + name + ">>", "");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fooDefinition, Map.of("revision", "1"));
+ assertThat(spelTemplateEngine.getValue(fooDefinition.getFirst(), String.class)).isEqualTo("fighters");
+
+ FakeDefinition barDefinition = new FakeDefinition("123", "<<" + name + ">>", "");
+ cut.onDefinitionDeploy(BAR_ENV_ID, barDefinition, Map.of("revision", "1"));
+ assertThat(spelTemplateEngine.getValue(barDefinition.getFirst(), String.class)).isEqualTo("tender");
+ }
+
+ @Test
+ void should_discover_secrets_in_two_locations() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("dog");
+ }
+
+ @Test
+ void should_discover_deny_access_to_second_secrets_due_to_ACLs() {
+ Spec spec = new Spec(
+ null,
+ null,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class);
+ }
+
+ @Test
+ void should_fail_getting_secret_secrets_after_spec_undeployed() {
+ String name = "redis-password";
+ Spec spec = new Spec(null, name, "/mock/mySecret", "redisPassword", null, false, false, null, null, FOO_ENV_ID);
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<<" + name + ">>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ specLifeCycleService.undeploy(spec);
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class);
+ }
+
+ @Test
+ void should_fail_to_resolve_secret_on_the_fly_then_succeeds_after_secret_it_is_present() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<< /mock/flaky:password>>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/flaky")))
+ .isPresent()
+ .get()
+ .extracting(Entry::type)
+ .isEqualTo(Entry.Type.ERROR);
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class))
+ .isInstanceOf(SecretProviderException.class)
+ .hasMessageContaining("huge error!!!");
+
+ // it should be there any milliseconds
+ await()
+ .atMost(500, TimeUnit.MILLISECONDS)
+ .untilAsserted(() ->
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).doesNotThrowAnyException()
+ );
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("iamflaky");
+ }
+
+ @Test
+ void should_get_a_different_secret_after_spec_update() {
+ String name = "password";
+ String specID = UUID.randomUUID().toString();
+ Spec spec = new Spec(
+ specID,
+ name,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<<" + name + ">>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+
+ Spec specV2 = new Spec(
+ specID,
+ name,
+ "/mock/mySecret",
+ "ldapPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(specV2);
+ awaitShortly()
+ .untilAsserted(() -> assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("dog"));
+ }
+
+ @Test
+ void should_fail_getting_secret_when_acls_changes() {
+ String name = "password";
+ String specID = UUID.randomUUID().toString();
+ Spec spec = new Spec(
+ specID,
+ name,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<<" + name + ">>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+
+ Spec specV2 = new Spec(
+ specID,
+ name,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("second", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(specV2);
+ awaitShortly()
+ .untilAsserted(() ->
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class)
+ );
+ }
+
+ @Test
+ void should_fail_to_get_error_when_secret_provider_returns_error() {
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class))
+ .isInstanceOf(SecretProviderException.class)
+ .hasMessageContaining("I am not in the mood");
+ }
+
+ @Test
+ void should_fail_to_get_secret_after_undeploy() {
+ String name = "password";
+ String specID = UUID.randomUUID().toString();
+ Spec spec = new Spec(
+ specID,
+ name,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(spec);
+ awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<<" + name + ">>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+
+ specLifeCycleService.undeploy(spec);
+
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class);
+ }
+
+ @Test
+ void should_go_from_on_the_fly_to_named_user_flow() {
+ // on the fly
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("fighters");
+
+ // create spec to limit sage
+ String specID = UUID.randomUUID().toString();
+ Spec specWithUri = new Spec(
+ specID,
+ null,
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(specWithUri);
+ awaitShortly()
+ .untilAsserted(() -> {
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters");
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class);
+ });
+
+ // create spec to limit sage
+ Spec specWithName = new Spec(
+ specID,
+ "redis-password",
+ "/mock/mySecret",
+ "redisPassword",
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))),
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(specWithName);
+
+ awaitShortly()
+ .untilAsserted(() ->
+ assertThat(cache.get(CacheKey.from(specWithName))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE)
+ );
+
+ FakeDefinition fakeDefinition2 = new FakeDefinition("123", "<>", "<< redis-password>>");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition2, Map.of("revision", "2"));
+
+ await()
+ .atMost(1, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ assertThat(cache.get(CacheKey.from(specWithName))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE);
+ assertThat(spelTemplateEngine.getValue(fakeDefinition2.getFirst(), String.class)).isEqualTo("fighters");
+ assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition2.getSecond(), String.class))
+ .isInstanceOf(SecretAccessDeniedException.class);
+ });
+ }
+
+ @Test
+ void should_evict_unused_secrets_on_the_fly() {
+ // on the fly
+ FakeDefinition fakeDefinitionRev1 = new FakeDefinition(
+ "123",
+ "<>",
+ "<>"
+ );
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinitionRev1, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinitionRev1.getFirst(), String.class)).isEqualTo("fighters");
+ assertThat(spelTemplateEngine.getValue(fakeDefinitionRev1.getSecond(), String.class)).isEqualTo("yeah!");
+
+ FakeDefinition fakeDefinitionRev2 = new FakeDefinition("123", "<>", null);
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinitionRev2, Map.of("revision", "2"));
+ cut.onDefinitionUnDeploy(FOO_ENV_ID, fakeDefinitionRev1, Map.of("revision", "1"));
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinitionRev2.getFirst(), String.class)).isEqualTo("fighters");
+ assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/secondSecret"))).isNotPresent();
+ }
+
+ @Test
+ void should_renew_secrets() {
+ renewalService.start();
+
+ Spec spec = new Spec(
+ "123456",
+ "rotating",
+ "/mock/rotating",
+ "password",
+ null,
+ false,
+ false,
+ new Resolution(Resolution.Type.POLL, Duration.ofSeconds(1)),
+ null,
+ FOO_ENV_ID
+ );
+ specLifeCycleService.deploy(spec);
+
+ await()
+ .atMost(1, TimeUnit.SECONDS)
+ .untilAsserted(() ->
+ assertThat(cache.get(CacheKey.from(spec))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE)
+ );
+
+ FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>");
+ cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1"));
+ await()
+ .atMost(1, TimeUnit.SECONDS)
+ .untilAsserted(() ->
+ assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/secondSecret")))
+ .isPresent()
+ .get()
+ .extracting(Entry::type)
+ .isEqualTo(Entry.Type.VALUE)
+ );
+
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("secret1");
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("yeah!");
+
+ await()
+ .atMost(2, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("secret2");
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("yeah!");
+ });
+ await()
+ .atMost(2, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("secret3");
+ assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("yeah!");
+ });
+ }
+
+ static class TestDefinitionBrowser implements DefinitionBrowser {
+
+ @Override
+ public boolean canHandle(Object definition) {
+ return definition instanceof FakeDefinition;
+ }
+
+ @Override
+ public Definition getDefinitionLocation(FakeDefinition definition, Map metadata) {
+ return new Definition("test", definition.getId(), Optional.of(metadata.get("revision")));
+ }
+
+ @Override
+ public void findPayloads(FakeDefinition definition, DefinitionPayloadNotifier notifier) {
+ if (definition.getFirst() != null) {
+ notifier.onPayload(definition.getFirst(), new PayloadLocation(PayloadLocation.PLUGIN_KIND, "first"), definition::setFirst);
+ }
+ if (definition.getSecond() != null) {
+ notifier.onPayload(
+ definition.getSecond(),
+ new PayloadLocation(PayloadLocation.PLUGIN_KIND, "second"),
+ definition::setSecond
+ );
+ }
+ }
+ }
+
+ ConditionFactory awaitShortly() {
+ return await().pollDelay(0, TimeUnit.MILLISECONDS).pollInterval(20, TimeUnit.MILLISECONDS).atMost(100, TimeUnit.MILLISECONDS);
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class FakeDefinition {
+
+ private String id, first, second;
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefDiscovererTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefDiscovererTest.java
new file mode 100644
index 000000000..36507e4e4
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefDiscovererTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class RefDiscovererTest {
+
+ public static Stream payloads() {
+ return Stream.of(
+ arguments(
+ "one smaller",
+ """
+ {
+ "username": "admin",
+ "password": "<< uri /vault/secrets/redis:password >>"
+ }
+ """,
+ List.of("<< uri /vault/secrets/redis:password >>"),
+ List.of("secret"),
+ List.of("\"password\": \"secret\"")
+ ),
+ arguments(
+ "one bigger",
+ """
+ {
+ "username": "admin",
+ "password": "<< uri /vault/secrets/redis:password >>"
+ }
+ """,
+ List.of("<< uri /vault/secrets/redis:password >>"),
+ List.of(
+ "{#secret.fromGrant('262fc907-ef40-47e0-b076-001ca79282da', 'f49ea02d-cd44-44dd-aa6d-04bb2a57a6ac-/vault/secrets/redis', 'password')}"
+ ),
+ List.of(
+ "\"password\": \"{#secret.fromGrant('262fc907-ef40-47e0-b076-001ca79282da', 'f49ea02d-cd44-44dd-aa6d-04bb2a57a6ac-/vault/secrets/redis', 'password')}\""
+ )
+ ),
+ arguments(
+ "mixed bigger",
+ """
+ {
+ "username": "admin",
+ "password": "<< uri /vault/secrets/redis:password >>"
+ "token": "<< name redis-token >>"
+ }
+ """,
+ List.of("<< uri /vault/secrets/redis:password >>", "<< name redis-token >>"),
+ List.of(
+ "{#secret.fromGrant('ce06fe04-3cd4-4513-bf15-5aa446ab2c27', 'f49ea02d-cd44-44dd-aa6d-04bb2a57a6ac-/vault/secrets/redis', 'password')}",
+ "ABCDEFGH"
+ ),
+ List.of(
+ "\"password\": \"{#secret.fromGrant('ce06fe04-3cd4-4513-bf15-5aa446ab2c27', 'f49ea02d-cd44-44dd-aa6d-04bb2a57a6ac-/vault/secrets/redis', 'password')}\"",
+ "\"token\": \"ABCDEFGH\""
+ )
+ )
+ );
+ }
+
+ @MethodSource("payloads")
+ @ParameterizedTest(name = "{0}")
+ void should_replace_refs_in_payloads(
+ String name,
+ String payload,
+ List refs,
+ List replacements,
+ List testString
+ ) {
+ PayloadRefParser disco = new PayloadRefParser(payload);
+ disco.runDiscovery();
+ assertThat(disco.getRawRefs().stream().map(PayloadRefParser.RawSecretRef::ref)).containsAnyElementsOf(refs);
+ String result = disco.replaceRefs(replacements);
+ assertThat(result).contains(testString);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java
new file mode 100644
index 000000000..a7bb81252
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.discovery;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class RefParserTest {
+
+ public static Stream okRefs() {
+ return Stream.of(
+ arguments(
+ "static uri short",
+ "<< /vault/secrets/partners/apikeys:tesco >>",
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/vault/secrets/partners/apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("tesco", false),
+ "<< /vault/secrets/partners/apikeys:tesco >>"
+ )
+ ),
+ arguments(
+ "static uri prefix",
+ "<< uri /vault/secrets/partners/apikeys:tesco >>",
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/vault/secrets/partners/apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("tesco", false),
+ "<< uri /vault/secrets/partners/apikeys:tesco >>"
+ )
+ ),
+ arguments(
+ "static uri long",
+ "<< uri /vault/secrets/partners/apikeys key tesco >>",
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/vault/secrets/partners/apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("tesco", false),
+ "<< uri /vault/secrets/partners/apikeys key tesco >>"
+ )
+ ),
+ arguments(
+ "static name short",
+ "<< partners-apikeys >>",
+ new Ref(Ref.MainType.NAME, new Ref.Expression("partners-apikeys", false), null, null, "<< partners-apikeys >>")
+ ),
+ arguments(
+ "static name prefix",
+ "<< name partners-apikeys >>",
+ new Ref(Ref.MainType.NAME, new Ref.Expression("partners-apikeys", false), null, null, "<< name partners-apikeys >>")
+ ),
+ arguments(
+ "dyn EL key",
+ "<< uri /vault/secrets/partners/apikeys key {#context.attributes['secret-key']} >>",
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/vault/secrets/partners/apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("#context.attributes['secret-key']", true),
+ "<< uri /vault/secrets/partners/apikeys key {#context.attributes['secret-key']} >>"
+ )
+ ),
+ arguments(
+ "dyn EL key short",
+ "<< /vault/secrets/partners/apikeys:{#context.attributes['secret-key']} >>",
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/vault/secrets/partners/apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("#context.attributes['secret-key']", true),
+ "<< /vault/secrets/partners/apikeys:{#context.attributes['secret-key']} >>"
+ )
+ ),
+ arguments(
+ "name and dyn EL key short",
+ "<< partners-apikeys:{#context.attributes['secret-key']} >>",
+ new Ref(
+ Ref.MainType.NAME,
+ new Ref.Expression("partners-apikeys", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression("#context.attributes['secret-key']", true),
+ "<< partners-apikeys:{#context.attributes['secret-key']} >>"
+ )
+ ),
+ arguments(
+ "dynamic name",
+ "<< name #context.attributes['secret-uri'] >>",
+ new Ref(
+ Ref.MainType.NAME,
+ new Ref.Expression("#context.attributes['secret-uri']", true),
+ null,
+ null,
+ "<< name #context.attributes['secret-uri'] >>"
+ )
+ )
+ );
+ }
+
+ @MethodSource("okRefs")
+ @ParameterizedTest(name = "{0}")
+ void should_parse(String name, String given, Ref expected) {
+ assertThat(RefParser.parse(given)).usingRecursiveComparison().isEqualTo(expected);
+ }
+
+ @Test
+ void should_parse_uri_and_key() {
+ String expression = "/provider/secret:password";
+ assertThat(RefParser.parseUriAndKey(expression))
+ .usingRecursiveComparison()
+ .isEqualTo(new RefParser.UriAndKey("/provider/secret", "password"));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/el/ServiceTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/el/ServiceTest.java
new file mode 100644
index 000000000..3d6a51def
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/el/ServiceTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.el;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.config.OnTheFlySpecs;
+import com.graviteesource.services.runtimesecrets.config.Renewal;
+import com.graviteesource.services.runtimesecrets.config.Retry;
+import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry;
+import com.graviteesource.services.runtimesecrets.discovery.RefParser;
+import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine;
+import com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider;
+import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService;
+import com.graviteesource.services.runtimesecrets.grant.GrantRegistry;
+import com.graviteesource.services.runtimesecrets.providers.DefaultResolverService;
+import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry;
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import com.graviteesource.services.runtimesecrets.spec.DefaultSpecLifecycleService;
+import com.graviteesource.services.runtimesecrets.spec.SpecRegistry;
+import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache;
+import io.gravitee.el.spel.SpelExpressionParser;
+import io.gravitee.el.spel.SpelTemplateEngine;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryLocation;
+import io.gravitee.node.api.secrets.runtime.grant.GrantService;
+import io.gravitee.node.api.secrets.runtime.providers.ResolverService;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.secrets.plugin.mock.MockSecretProvider;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import org.awaitility.core.ConditionFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.security.util.InMemoryResource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class ServiceTest {
+
+ public static final String ENV_ID = "foo";
+ InMemoryResource inMemoryResource = new InMemoryResource(
+ """
+ secrets:
+ mySecret:
+ redisPassword: "redisadmin"
+ ldapPassword: "ldapadmin"
+
+ """
+ );
+ private GrantService grantService;
+ private SpecLifecycleService specLifeCycleService;
+ private Cache cache;
+ private SpelTemplateEngine spelTemplateEngine;
+
+ @BeforeEach
+ void before() {
+ final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+ yaml.setResources(inMemoryResource);
+ SecretProviderRegistry secretProviderRegistry = new SecretProviderRegistry();
+ secretProviderRegistry.register(
+ "mock",
+ new MockSecretProvider(new MockSecretProviderConfiguration((Map) new LinkedHashMap<>(yaml.getObject()))),
+ null
+ );
+ cache = new SimpleOffHeapCache();
+ Config config = new Config(new OnTheFlySpecs(true, Duration.ZERO), Retry.none(), new Renewal(true, Duration.ZERO));
+ this.grantService = new DefaultGrantService(new GrantRegistry(), config);
+ SpecRegistry specRegistry = new SpecRegistry();
+ ResolverService resolverService = new DefaultResolverService(secretProviderRegistry, cache, config);
+ RenewalService renewalService = new RenewalService(resolverService, cache, config);
+ specLifeCycleService =
+ new DefaultSpecLifecycleService(
+ specRegistry,
+ new DefaultContextRegistry(),
+ cache,
+ resolverService,
+ grantService,
+ renewalService,
+ config
+ );
+ SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider(
+ cache,
+ grantService,
+ specLifeCycleService,
+ specRegistry
+ );
+ spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser());
+ // set up EL variables
+ secretsTemplateVariableProvider.provide(spelTemplateEngine.getTemplateContext());
+ spelTemplateEngine.getTemplateContext().setVariable("keys", Map.of("redis", "redisPassword"));
+ spelTemplateEngine.getTemplateContext().setVariable("names", Map.of("redis", "redis-password"));
+ spelTemplateEngine.getTemplateContext().setVariable("uris", Map.of("redis", "/mock/mySecret:redisPassword"));
+ }
+
+ @CsvSource(
+ value = {
+ "by name, redis-password, redis-password, redisPassword, false, <>",
+ "by uri, null, /mock/mySecret, redisPassword, false, <>",
+ "by name with EL, redis-password, redis-password, null, true, <>",
+ "by uri with EL, null, /mock/mySecret, null, true, <>",
+ },
+ nullValues = "null"
+ )
+ @ParameterizedTest(name = "{0}")
+ void should_call_service_using_fromGrant(
+ String test,
+ String specName,
+ String naturalId,
+ String key,
+ boolean dynKeys,
+ String refAsString
+ ) {
+ Spec spec = new Spec(null, specName, "/mock/mySecret", key, null, dynKeys, false, null, null, ENV_ID);
+ specLifeCycleService.deploy(spec);
+ shortAwait().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+
+ DiscoveryContext context = new DiscoveryContext(
+ UUID.randomUUID(),
+ ENV_ID,
+ RefParser.parse(refAsString),
+ new DiscoveryLocation(new DiscoveryLocation.Definition("test", "123"))
+ );
+ boolean authorized = grantService.grant(context, spec);
+ assertThat(authorized).isTrue();
+
+ String el = Formatter.computeELFromStatic(context, ENV_ID);
+ assertThat(spelTemplateEngine.getValue(el, String.class)).isEqualTo("redisadmin");
+ }
+
+ @CsvSource(
+ value = {
+ "by name, redis-password, redis-password, <>, true",
+ "by uri, null, /mock/mySecret, <>, true",
+ "by uri on the fly, null, null, <>, false",
+ },
+ nullValues = "null"
+ )
+ @ParameterizedTest(name = "{0}")
+ void should_call_service_using_fromELWith(String test, String specName, String naturalId, String refAsString, boolean createSpec) {
+ DiscoveryContext context = new DiscoveryContext(
+ UUID.randomUUID(),
+ ENV_ID,
+ RefParser.parse(refAsString),
+ new DiscoveryLocation(new DiscoveryLocation.Definition("test", "123"))
+ );
+
+ if (createSpec) {
+ Spec spec = new Spec(null, specName, "/mock/mySecret", "redisPassword", null, false, false, null, null, ENV_ID);
+ specLifeCycleService.deploy(spec);
+ shortAwait().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent());
+ boolean authorized = grantService.grant(context, spec);
+ assertThat(authorized).isTrue();
+ grantService.grant(context, spec);
+ }
+
+ String el = Formatter.computeELFromEL(context, ENV_ID);
+ assertThat(spelTemplateEngine.getValue(el, String.class)).isEqualTo("redisadmin");
+ }
+
+ ConditionFactory shortAwait() {
+ return await().pollDelay(0, TimeUnit.MILLISECONDS).pollInterval(20, TimeUnit.MILLISECONDS).atMost(100, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantServiceTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantServiceTest.java
new file mode 100644
index 000000000..3cc71618a
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantServiceTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.grant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.config.OnTheFlySpecs;
+import com.graviteesource.services.runtimesecrets.config.Renewal;
+import com.graviteesource.services.runtimesecrets.config.Retry;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext;
+import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryLocation;
+import io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.spec.ACLs;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class DefaultGrantServiceTest {
+
+ private DefaultGrantService cut;
+
+ @BeforeEach
+ void setup() {
+ Config config = new Config(new OnTheFlySpecs(true, Duration.ZERO), Retry.none(), new Renewal(true, Duration.ZERO));
+ this.cut = new DefaultGrantService(new GrantRegistry(), config);
+ }
+
+ public static Stream grants() {
+ return Stream.of(
+ arguments("no acl same env", context("dev", "api", "123"), spec("dev", null, null)),
+ arguments("no acl same key", context("dev", "api", "123", "pwd"), spec("dev", null, "pwd")),
+ arguments("empty acl same env", context("dev", "api", "123"), spec("dev", new ACLs(null, List.of(), List.of()), null)),
+ arguments(
+ "def acl ok",
+ context("dev", "api", "123"),
+ spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), null)
+ ),
+ arguments(
+ "def acl ok same key",
+ context("dev", "api", "123", "pwd"),
+ spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pwd")
+ ),
+ arguments(
+ "def acl ok many",
+ context("dev", "api", "123"),
+ spec(
+ "dev",
+ new ACLs(
+ null,
+ List.of(new ACLs.DefinitionACL("dict", List.of("123")), new ACLs.DefinitionACL("api", List.of("123", "456"))),
+ null
+ ),
+ null
+ )
+ ),
+ arguments(
+ "plugin acl ok",
+ context("dev", "api", "123", plugin("foo")),
+ spec(
+ "dev",
+ new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null))),
+ null
+ )
+ ),
+ arguments(
+ "plugin acl ok many",
+ context("dev", "api", "123", plugin("foo")),
+ spec(
+ "dev",
+ new ACLs(
+ null,
+ List.of(new ACLs.DefinitionACL("api", List.of("123"))),
+ List.of(new ACLs.PluginACL("bar", null), new ACLs.PluginACL("foo", null))
+ ),
+ null
+ )
+ ),
+ arguments(
+ "plugin acl only",
+ context("dev", "api", "123", plugin("foo")),
+ spec("dev", new ACLs(null, null, List.of(new ACLs.PluginACL("foo", null))), null)
+ ),
+ arguments(
+ "plugin acl only many",
+ context("dev", "api", "123", plugin("foo")),
+ spec(
+ "dev",
+ new ACLs(
+ null,
+ List.of(), // setting an empty list for the sake of testing empty list
+ List.of(new ACLs.PluginACL("bar", null), new ACLs.PluginACL("foo", null))
+ ),
+ null
+ )
+ )
+ );
+ }
+
+ public static Stream denials() {
+ return Stream.of(
+ arguments("no acl diff env", context("dev", "api", "123"), spec("test", null, null)),
+ arguments("no acl diff key", context("dev", "api", "123", "pwd"), spec("dev", null, "pass")),
+ arguments(
+ "def acl ok wrong env",
+ context("dev", "api", "123"),
+ spec("test", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), null)
+ ),
+ arguments(
+ "def acl ok wrong key",
+ context("dev", "api", "123", "pwd"),
+ spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pass")
+ ),
+ arguments(
+ "def acl wrong id",
+ context("dev", "api", "123"),
+ spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("456"))), null), null)
+ ),
+ arguments(
+ "def acl wrong kind",
+ context("dev", "api", "123"),
+ spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("dict", List.of("123"))), null), null)
+ ),
+ arguments(
+ "plugin acl ko",
+ context("dev", "api", "123", plugin("foo")),
+ spec(
+ "dev",
+ new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("bar", null))),
+ null
+ )
+ ),
+ arguments(
+ "plugin acl only ko",
+ context("dev", "api", "123", plugin("bar")),
+ spec("dev", new ACLs(null, null, List.of(new ACLs.PluginACL("foo", null))), null)
+ ),
+ arguments("no spec", context("dev", "api", "123", plugin("bar")), null)
+ );
+ }
+
+ @MethodSource("grants")
+ @ParameterizedTest(name = "{0}")
+ void should_grant(String name, DiscoveryContext context, Spec spec) {
+ assertThat(cut.grant(context, spec)).isTrue();
+ }
+
+ @MethodSource("denials")
+ @ParameterizedTest(name = "{0}")
+ void should_deny(String name, DiscoveryContext context, Spec spec) {
+ assertThat(cut.grant(context, spec)).isFalse();
+ }
+
+ static DiscoveryContext context(String env, String kind, String id, String key, PayloadLocation... payloads) {
+ return context(
+ env,
+ kind,
+ id,
+ new Ref(
+ Ref.MainType.URI,
+ new Ref.Expression("/mock/secret", false),
+ Ref.SecondaryType.KEY,
+ new Ref.Expression(key, false),
+ "<< /mock/secret:%s >>".formatted(key)
+ ),
+ payloads
+ );
+ }
+
+ static DiscoveryContext context(String env, String kind, String id, PayloadLocation... payloads) {
+ return context(
+ env,
+ kind,
+ id,
+ new Ref(Ref.MainType.NAME, new Ref.Expression("secret", false), null, null, "<< secret >>"),
+ payloads
+ );
+ }
+
+ static DiscoveryContext context(String env, String kind, String id, Ref ref, PayloadLocation... payloads) {
+ return new DiscoveryContext(null, env, ref, new DiscoveryLocation(new DiscoveryLocation.Definition(kind, id), payloads));
+ }
+
+ static Spec spec(String env, ACLs acls, String key) {
+ return new Spec(null, "secret", null, key, null, key == null, false, null, acls, env);
+ }
+
+ static PayloadLocation plugin(String id) {
+ return new PayloadLocation(PayloadLocation.PLUGIN_KIND, id);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployerTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployerTest.java
new file mode 100644
index 000000000..49f932277
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.providers;
+
+import com.graviteesource.services.runtimesecrets.providers.config.FromConfigurationSecretProviderDeployer;
+import com.graviteesource.services.runtimesecrets.testsupport.PluginManagerHelper;
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.api.secrets.model.SecretMount;
+import io.gravitee.node.secrets.plugin.mock.MockSecretProvider;
+import io.gravitee.node.secrets.plugins.SecretProviderPluginManager;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.mock.env.MockEnvironment;
+import org.springframework.security.util.InMemoryResource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class FromConfigurationSecretProviderDeployerTest {
+
+ InMemoryResource inMemoryResource = new InMemoryResource(
+ """
+ api:
+ secrets:
+ providers:
+ - plugin: "mock"
+ environments:
+ - "dev"
+ configuration:
+ enabled: true
+ secrets:
+ mySecret:
+ redisPassword: "foo"
+ ldapPassword: "bar"
+ - id: "all-env-secret-manager"
+ plugin: "mock"
+ configuration:
+ enabled: true
+ secrets:
+ my_secret:
+ redisPassword: "very-long-password"
+ ldapPassword: "also-quite-not-short-password"
+ - id: "disabled"
+ plugin: "mock"
+ configuration:
+ enabled: false
+
+ """
+ );
+ private SecretProviderRegistry registry;
+ private FromConfigurationSecretProviderDeployer cut;
+
+ @BeforeEach
+ void before() {
+ final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+ yaml.setResources(inMemoryResource);
+ MockEnvironment mockEnvironment = new MockEnvironment();
+ mockEnvironment.getPropertySources().addFirst(new MapPropertySource("test", new LinkedHashMap(yaml.getObject())));
+ registry = new SecretProviderRegistry();
+ SecretProviderPluginManager pluginManager = PluginManagerHelper.newPluginManagerWithMockPlugin();
+ cut = new FromConfigurationSecretProviderDeployer(mockEnvironment, registry, pluginManager);
+ }
+
+ @Test
+ void should_load_providers() {
+ cut.init();
+ AtomicReference last = new AtomicReference<>();
+ registry
+ .get("bar", "all-env-secret-manager")
+ .test()
+ .awaitCount(1)
+ .assertValue(sp -> {
+ last.set(sp);
+ return sp instanceof MockSecretProvider;
+ });
+ registry
+ .get("foo", "all-env-secret-manager")
+ .test()
+ .awaitCount(1)
+ .assertValue(sp -> sp instanceof MockSecretProvider && sp == last.get());
+ registry.get("dev", "mock").test().awaitCount(1).assertValue(sp -> sp instanceof MockSecretProvider && last.get() != sp);
+ registry
+ .get("test", "mock")
+ .flatMapMaybe(sp -> sp.resolve(new SecretMount("mock", null, "", null, false)))
+ .test()
+ .assertError(err -> err.getMessage().contains("for provider: 'mock'"));
+ registry
+ .get("any", "disabled")
+ .flatMapMaybe(sp -> sp.resolve(new SecretMount("mock", null, "", null, false)))
+ .test()
+ .assertError(err -> err.getMessage().contains("for provider: 'mock'"));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleServiceTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleServiceTest.java
new file mode 100644
index 000000000..82e78e312
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleServiceTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.spec;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.graviteesource.services.runtimesecrets.config.Config;
+import com.graviteesource.services.runtimesecrets.config.OnTheFlySpecs;
+import com.graviteesource.services.runtimesecrets.config.Renewal;
+import com.graviteesource.services.runtimesecrets.config.Retry;
+import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry;
+import com.graviteesource.services.runtimesecrets.discovery.RefParser;
+import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService;
+import com.graviteesource.services.runtimesecrets.grant.GrantRegistry;
+import com.graviteesource.services.runtimesecrets.providers.DefaultResolverService;
+import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry;
+import com.graviteesource.services.runtimesecrets.renewal.RenewalService;
+import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache;
+import io.gravitee.node.api.secrets.model.Secret;
+import io.gravitee.node.api.secrets.runtime.discovery.Ref;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import io.gravitee.node.secrets.plugin.mock.MockSecretProvider;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.security.util.InMemoryResource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class DefaultSpecLifecycleServiceTest {
+
+ public static final String ENV_ID = "foo";
+ InMemoryResource inMemoryResource = new InMemoryResource(
+ """
+ secrets:
+ mySecret:
+ redisPassword: "redisadmin"
+ ldapPassword: "ldapadmin"
+
+ """
+ );
+
+ SpecLifecycleService cut;
+ private SimpleOffHeapCache cache;
+
+ @BeforeEach
+ void before() {
+ final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+ yaml.setResources(inMemoryResource);
+ SecretProviderRegistry registry = new SecretProviderRegistry();
+ registry.register(
+ "mock",
+ new MockSecretProvider(new MockSecretProviderConfiguration((Map) new LinkedHashMap<>(yaml.getObject()))),
+ null
+ );
+ cache = new SimpleOffHeapCache();
+ Config config = new Config(new OnTheFlySpecs(true, Duration.ZERO), Retry.none(), new Renewal(true, Duration.ZERO));
+ RenewalService renewalService = new RenewalService(null, cache, config);
+ cut =
+ new DefaultSpecLifecycleService(
+ new SpecRegistry(),
+ new DefaultContextRegistry(),
+ cache,
+ new DefaultResolverService(registry, cache, config),
+ new DefaultGrantService(new GrantRegistry(), config),
+ renewalService,
+ config
+ );
+ }
+
+ @Test
+ void should_deploy_spec_and_get_secret_map_from_cache() {
+ Spec spec = new Spec(null, "redis-password", "/mock/mySecret", "redisPassword", null, false, false, null, null, ENV_ID);
+ cut.deploy(spec);
+ Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec));
+ }
+
+ @Test
+ void should_deploy_spec_on_the_fly_then_get_secret_map() {
+ Ref ref = RefParser.parse("<>");
+ assertThat(cut.shouldDeployOnTheFly(ref)).isTrue();
+ Spec spec = cut.deployOnTheFly(ENV_ID, ref, false);
+ assertThat(spec.uri()).isEqualTo("/mock/mySecret");
+ assertThat(spec.key()).isEqualTo("redisPassword");
+ Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "<>", "<>" })
+ void should_not_deploy_on_the_fly(String s) {
+ assertThat(cut.shouldDeployOnTheFly(RefParser.parse(s))).isFalse();
+ }
+
+ @Test
+ void should_deploy_spec_and_get_secret_map_from_cache_un_deploy_check_cache_empty() {
+ Spec spec = new Spec(null, "redis-password", "/mock/mySecret", "redisPassword", null, false, false, null, null, ENV_ID);
+ cut.deploy(spec);
+ Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec));
+ cut.undeploy(spec);
+ assertThat(cache.get(CacheKey.from(spec))).isNotPresent();
+ }
+
+ private void checkInCache(Spec spec) {
+ Optional foo = cache.get(CacheKey.from(spec));
+ assertThat(foo).get().extracting(Entry::type).asString().isEqualTo("VALUE");
+ assertThat(foo)
+ .get()
+ .extracting(Entry::value)
+ .asInstanceOf(InstanceOfAssertFactories.MAP)
+ .containsEntry("redisPassword", new Secret("redisadmin"))
+ .containsEntry("ldapPassword", new Secret("ldapadmin"));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCacheTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCacheTest.java
new file mode 100644
index 000000000..d0636e58e
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCacheTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.storage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.gravitee.node.api.secrets.model.Secret;
+import io.gravitee.node.api.secrets.runtime.storage.Cache;
+import io.gravitee.node.api.secrets.runtime.storage.CacheKey;
+import io.gravitee.node.api.secrets.runtime.storage.Entry;
+import java.util.Map;
+import java.util.Optional;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class SimpleOffHeapCacheTest {
+
+ private final Cache cut = new SimpleOffHeapCache();
+
+ @Test
+ void should_store_error_entries() {
+ cut.put(new CacheKey("dev", "secret_error"), new Entry(Entry.Type.ERROR, null, "500"));
+ cut.put(new CacheKey("test", "secret_empty"), new Entry(Entry.Type.EMPTY, null, "204"));
+ cut.put(new CacheKey("prod", "secret_not_found"), new Entry(Entry.Type.NOT_FOUND, null, "404"));
+
+ assertThat(cut.get(new CacheKey("dev", "secret_error"))).get().extracting("type", "error").containsExactly(Entry.Type.ERROR, "500");
+ assertThat(cut.get(new CacheKey("test", "secret_empty")))
+ .get()
+ .extracting("type", "error")
+ .containsExactly(Entry.Type.EMPTY, "204");
+ assertThat(cut.get(new CacheKey("prod", "secret_not_found")))
+ .isPresent()
+ .get()
+ .extracting("type", "error")
+ .containsExactly(Entry.Type.NOT_FOUND, "404");
+ }
+
+ @Test
+ void should_store_segmented_data() {
+ cut.put(new CacheKey("dev", "secret"), new Entry(Entry.Type.VALUE, Map.of("foo", new Secret("bar")), null));
+ cut.put(new CacheKey("test", "secret"), new Entry(Entry.Type.VALUE, Map.of("buz", new Secret("puk")), null));
+ assertThat(cut.get(new CacheKey("dev", "secret")))
+ .get()
+ .usingRecursiveAssertion()
+ .isEqualTo(new Entry(Entry.Type.VALUE, Map.of("foo", new Secret("bar")), null));
+ assertThat(cut.get(new CacheKey("test", "secret")))
+ .get()
+ .usingRecursiveAssertion()
+ .isEqualTo(new Entry(Entry.Type.VALUE, Map.of("buz", new Secret("puk")), null));
+ }
+
+ @Test
+ void should_perform_crud_ops() {
+ cut.put(
+ new CacheKey("dev", "secret"),
+ new Entry(Entry.Type.VALUE, Map.of("redis-password", new Secret("123456"), "ldap-password", new Secret("azerty")), null)
+ );
+ assertThat(cut.get(new CacheKey("dev", "secret")))
+ .get()
+ .extracting(entry -> entry.value().values().stream().map(Secret::asString).toList())
+ .asInstanceOf(InstanceOfAssertFactories.LIST)
+ .containsExactlyInAnyOrder("123456", "azerty");
+ assertThat(cut.get(new CacheKey("dev", "secret")))
+ .get()
+ .extracting(entry -> entry.value().keySet())
+ .asInstanceOf(InstanceOfAssertFactories.COLLECTION)
+ .containsExactlyInAnyOrder("redis-password", "ldap-password");
+
+ // override and test it was really done
+ Entry dbPasswords = new Entry(
+ Entry.Type.VALUE,
+ Map.of("mongodb-password", new Secret("778899"), "mysql-password", new Secret("qwerty")),
+ null
+ );
+ cut.put(new CacheKey("dev", "secret"), dbPasswords);
+ dbPasswordsAssert(cut.get(new CacheKey("dev", "secret")));
+
+ // no override as does not exists
+ cut.computeIfAbsent(new CacheKey("dev", "secret"), () -> new Entry(Entry.Type.VALUE, Map.of(), null));
+ dbPasswordsAssert(cut.get(new CacheKey("dev", "secret")));
+
+ // eviction
+ cut.evict(new CacheKey("dev", "secret"));
+ assertThat(cut.get(new CacheKey("dev", "secret"))).isNotPresent();
+
+ cut.computeIfAbsent(new CacheKey("dev", "secret"), () -> dbPasswords);
+ dbPasswordsAssert(cut.get(new CacheKey("dev", "secret")));
+ }
+
+ private void dbPasswordsAssert(Optional optEntry) {
+ assertThat(optEntry)
+ .get()
+ .extracting(entry -> entry.value().values().stream().map(Secret::asString).toList())
+ .asInstanceOf(InstanceOfAssertFactories.LIST)
+ .containsExactlyInAnyOrder("778899", "qwerty");
+ assertThat(optEntry)
+ .get()
+ .extracting(entry -> entry.value().keySet())
+ .asInstanceOf(InstanceOfAssertFactories.COLLECTION)
+ .containsExactlyInAnyOrder("mongodb-password", "mysql-password");
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/PluginManagerHelper.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/PluginManagerHelper.java
new file mode 100644
index 000000000..5da101875
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/PluginManagerHelper.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.testsupport;
+
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.secrets.plugin.mock.MockSecretProviderFactory;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import io.gravitee.node.secrets.plugins.SecretProviderPlugin;
+import io.gravitee.node.secrets.plugins.SecretProviderPluginManager;
+import io.gravitee.node.secrets.plugins.internal.DefaultSecretProviderClassLoaderFactory;
+import io.gravitee.node.secrets.plugins.internal.DefaultSecretProviderPlugin;
+import io.gravitee.node.secrets.plugins.internal.DefaultSecretProviderPluginManager;
+import io.gravitee.plugin.core.api.PluginManifest;
+import java.net.URL;
+import java.nio.file.Path;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class PluginManagerHelper {
+
+ public static SecretProviderPluginManager newPluginManagerWithMockPlugin() {
+ SecretProviderPluginManager pluginManager = new DefaultSecretProviderPluginManager(new DefaultSecretProviderClassLoaderFactory());
+ pluginManager.register(
+ new DefaultSecretProviderPlugin<>(
+ new MockSecretProviderPlugin(true),
+ MockSecretProviderFactory.class,
+ MockSecretProviderConfiguration.class
+ )
+ );
+ return pluginManager;
+ }
+
+ public static class MockSecretProviderPlugin
+ implements SecretProviderPlugin {
+
+ private final boolean deployed;
+
+ public MockSecretProviderPlugin(boolean deployed) {
+ this.deployed = deployed;
+ }
+
+ @Override
+ public String id() {
+ return "mock";
+ }
+
+ @Override
+ public String clazz() {
+ return MockSecretProviderFactory.class.getName();
+ }
+
+ @Override
+ public Class secretProviderFactory() {
+ return MockSecretProviderFactory.class;
+ }
+
+ @Override
+ public Path path() {
+ return Path.of("src/test/resources");
+ }
+
+ @Override
+ public PluginManifest manifest() {
+ return new PluginManifest() {
+ @Override
+ public String id() {
+ return "mock";
+ }
+
+ @Override
+ public String name() {
+ return "Mock Secret Provider";
+ }
+
+ @Override
+ public String description() {
+ return "Mock Secret Provider";
+ }
+
+ @Override
+ public String category() {
+ return "secret providers";
+ }
+
+ @Override
+ public String version() {
+ return "0.0.0";
+ }
+
+ @Override
+ public String plugin() {
+ return MockSecretProviderFactory.class.getName();
+ }
+
+ @Override
+ public String type() {
+ return SecretProvider.PLUGIN_TYPE;
+ }
+
+ @Override
+ public String feature() {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public URL[] dependencies() {
+ return new URL[0];
+ }
+
+ @Override
+ public boolean deployed() {
+ return this.deployed;
+ }
+
+ @Override
+ public Class configuration() {
+ return null;
+ }
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/SpecFixtures.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/SpecFixtures.java
new file mode 100644
index 000000000..d8677f438
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/SpecFixtures.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed 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 com.graviteesource.services.runtimesecrets.testsupport;
+
+import io.gravitee.node.api.secrets.runtime.spec.ACLs;
+import io.gravitee.node.api.secrets.runtime.spec.Spec;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class SpecFixtures {
+
+ public static Spec namedWithDynKey(String envId, String name, String uri) {
+ return new Spec(UUID.randomUUID().toString(), name, uri, null, null, true, false, null, null, envId);
+ }
+
+ public static Spec fromUriDynKey(String envId, String uri) {
+ return new Spec(UUID.randomUUID().toString(), null, uri, null, null, true, false, null, null, envId);
+ }
+
+ public static Spec fromUriAndKey(String envId, String uri, String key) {
+ return new Spec(UUID.randomUUID().toString(), null, uri, key, null, false, false, null, null, envId);
+ }
+
+ public static Spec namedWithUriAndKey(String envId, String name, String uri, String key) {
+ return new Spec(UUID.randomUUID().toString(), name, uri, key, null, false, false, null, null, envId);
+ }
+
+ public static Spec fromNameUriAndKeyACLs(
+ String envId,
+ String name,
+ String uri,
+ String key,
+ ACLs.DefinitionACL definitionACL,
+ ACLs.PluginACL pluginACL
+ ) {
+ return new Spec(
+ UUID.randomUUID().toString(),
+ name,
+ uri,
+ key,
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, List.of(definitionACL), List.of(pluginACL)),
+ envId
+ );
+ }
+
+ public static Spec fromNameUriAndKeyPluginACL(String envId, String name, String uri, String key, ACLs.PluginACL pluginACL) {
+ return new Spec(
+ UUID.randomUUID().toString(),
+ name,
+ uri,
+ key,
+ null,
+ false,
+ false,
+ null,
+ new ACLs(null, null, List.of(pluginACL)),
+ envId
+ );
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/logback.xml b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/logback.xml
new file mode 100644
index 000000000..36e8353a8
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/logback.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/v4-api.json b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/v4-api.json
new file mode 100644
index 000000000..af26f5a5e
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/v4-api.json
@@ -0,0 +1,269 @@
+{
+ "id": "7023b5ac-0fff-402e-a3b5-ac0fff402efe",
+ "name": "Test V4",
+ "type": "proxy",
+ "apiVersion": "1",
+ "definitionVersion": "4.0.0",
+ "tags": [],
+ "properties": [],
+ "resources": [
+ {
+ "name": "users",
+ "type": "auth-provider-inline-resource",
+ "configuration": {
+ "users": [
+ {
+ "username": "batman",
+ "password": "<< /mock/users:batman >>",
+ "roles": []
+ },
+ {
+ "username": "robin",
+ "password": "<< /mock/users:robin >>",
+ "roles": []
+ }
+ ]
+ },
+ "enabled": true
+ }
+ ],
+ "listeners": [
+ {
+ "type": "http",
+ "entrypoints": [
+ {
+ "type": "http-proxy",
+ "configuration": {},
+ "qos": "auto"
+ }
+ ],
+ "paths": [
+ {
+ "path": "/test-benoit-secrets/"
+ }
+ ]
+ }
+ ],
+ "endpointGroups": [
+ {
+ "name": "Default HTTP proxy group",
+ "type": "http-proxy",
+ "loadBalancer": {
+ "type": "round-robin"
+ },
+ "sharedConfiguration": {
+ "headers": [
+ {
+ "name": "Authorization",
+ "value": "Bearer << backend-bearer-token >>"
+ }
+ ],
+ "proxy": {
+ "useSystemProxy": false,
+ "enabled": false
+ },
+ "http": {
+ "keepAliveTimeout": 30000,
+ "keepAlive": true,
+ "followRedirects": false,
+ "readTimeout": 10000,
+ "idleTimeout": 60000,
+ "connectTimeout": 3000,
+ "useCompression": true,
+ "maxConcurrentConnections": 20,
+ "version": "HTTP_1_1",
+ "pipelining": false
+ },
+ "ssl": {
+ "keyStore": {
+ "certContent": "<< /mock/back-end-tls:crt >>",
+ "keyContent": "<< /mock/back-end-tls:key >>",
+ "type": "PEM"
+ },
+ "hostnameVerifier": true,
+ "trustStore": {
+ "type": ""
+ },
+ "trustAll": false
+ }
+ },
+ "endpoints": [
+ {
+ "name": "Default HTTP proxy",
+ "type": "http-proxy",
+ "secondary": false,
+ "weight": 1,
+ "inheritConfiguration": true,
+ "configuration": {
+ "target": "http://localhost:7777"
+ },
+ "sharedConfigurationOverride": {},
+ "services": {
+ "healthCheck": {
+ "overrideConfiguration": true,
+ "enabled": true,
+ "type": "http-health-check",
+ "configuration": {
+ "schedule": "0 */1 * * * *",
+ "headers": [
+ {
+ "name": "Authorization",
+ "value": "ApiKey << /mock/hc:apikey >>"
+ }
+ ],
+ "overrideEndpointPath": true,
+ "method": "GET",
+ "failureThreshold": 2,
+ "assertion": "{#response.status == 200}",
+ "successThreshold": 2,
+ "target": "/"
+ }
+ }
+ }
+ }
+ ],
+ "services": {}
+ }
+ ],
+ "analytics": {
+ "enabled": true
+ },
+ "plans": [
+ {
+ "id": "02ee8113-158c-4ff4-ae81-13158c7ff4f0",
+ "name": "Default Keyless (UNSECURED)",
+ "security": {
+ "type": "key-less",
+ "configuration": {}
+ },
+ "mode": "standard",
+ "tags": [],
+ "status": "published",
+ "flows": [
+ {
+ "id": "57444b7d-2bef-4370-844b-7d2bef737020",
+ "name": "plan",
+ "enabled": true,
+ "request": [
+ {
+ "name": "Assign attributes",
+ "enabled": true,
+ "policy": "policy-assign-attributes",
+ "configuration": {
+ "scope": "REQUEST",
+ "attributes": [
+ {
+ "name": "partner-id",
+ "value": "{#request.headers['X-Partner-Id']}"
+ }
+ ]
+ }
+ },
+ {
+ "name": "Transform Headers",
+ "enabled": true,
+ "policy": "transform-headers",
+ "configuration": {
+ "whitelistHeaders": [],
+ "addHeaders": [
+ {
+ "name": "X-Request-Plan",
+ "value": "<< /mock/headers:request-plan >>"
+ }
+ ],
+ "scope": "REQUEST",
+ "removeHeaders": []
+ }
+ }
+ ],
+ "response": [
+ {
+ "name": "Transform Headers",
+ "enabled": true,
+ "policy": "transform-headers",
+ "configuration": {
+ "whitelistHeaders": [],
+ "addHeaders": [
+ {
+ "name": "X-Response-Plan",
+ "value": "<< /mock/headers:response-plan >>"
+ }
+ ],
+ "scope": "REQUEST",
+ "removeHeaders": []
+ }
+ }
+ ],
+ "subscribe": [],
+ "publish": [],
+ "selectors": [
+ {
+ "type": "http",
+ "path": "/",
+ "pathOperator": "EQUALS",
+ "methods": []
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "flowExecution": {
+ "mode": "default",
+ "matchRequired": false
+ },
+ "flows": [
+ {
+ "id": "45bdee1c-41b7-4a79-bdee-1c41b75a790d",
+ "name": "flow",
+ "enabled": true,
+ "request": [
+ {
+ "name": "Transform Headers",
+ "enabled": true,
+ "policy": "transform-headers",
+ "configuration": {
+ "whitelistHeaders": [],
+ "addHeaders": [
+ {
+ "name": "X-Flow-Request",
+ "value": "<< /mock/headers:request-flow >>"
+ }
+ ],
+ "scope": "REQUEST",
+ "removeHeaders": []
+ }
+ }
+ ],
+ "response": [
+ {
+ "name": "Transform Headers",
+ "enabled": true,
+ "policy": "transform-headers",
+ "configuration": {
+ "whitelistHeaders": [],
+ "addHeaders": [
+ {
+ "name": "X-Flow-Response",
+ "value": "<< /mock/headers:response-plan >>"
+ }
+ ],
+ "scope": "REQUEST",
+ "removeHeaders": []
+ }
+ }
+ ],
+ "subscribe": [],
+ "publish": [],
+ "selectors": [
+ {
+ "type": "http",
+ "path": "/api",
+ "pathOperator": "EQUALS",
+ "methods": []
+ }
+ ]
+ }
+ ],
+ "responseTemplates": {}
+}
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/pom.xml b/gravitee-node-secrets/gravitee-node-secrets-service/pom.xml
index 0eb6a3eb9..3333b8222 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/pom.xml
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/pom.xml
@@ -56,8 +56,7 @@
spring-test
test
-
-
+
org.bouncycastle
bcprov-jdk18on
test
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/AbstractSecretProviderDispatcher.java b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/AbstractSecretProviderDispatcher.java
index 4cce523ec..d2713aece 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/AbstractSecretProviderDispatcher.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/AbstractSecretProviderDispatcher.java
@@ -16,6 +16,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
@@ -25,7 +26,7 @@
@Slf4j
public abstract class AbstractSecretProviderDispatcher implements SecretProviderDispatcher {
- public static final String SECRET_PROVIDER_NOT_FOUND_FOR_ID = "No secret-provider plugin found for provider id: '%s'";
+ public static final String SECRET_PROVIDER_NOT_FOUND_FOR_ID = "No secret-provider plugin found for provider: '%s'";
private final SecretProviderPluginManager secretProviderPluginManager;
private final Map secretProviders = new HashMap<>();
@@ -35,6 +36,10 @@ protected AbstractSecretProviderDispatcher(SecretProviderPluginManager secretPro
}
protected final void createAndRegister(String id) {
+ createAndRegister(id, sp -> secretProviders.put(id, sp));
+ }
+
+ protected final void createAndRegister(String id, Consumer register) {
try {
final SecretProviderPlugin, ?> secretProviderPlugin = secretProviderPluginManager.get(id);
final Class extends SecretManagerConfiguration> configurationClass = secretProviderPlugin.configuration();
@@ -44,7 +49,7 @@ protected final void createAndRegister(String id) {
SecretManagerConfiguration config =
this.readConfiguration(id, factory.getClass().getClassLoader().loadClass(configurationClass.getName()));
// register and start
- secretProviders.put(id, factory.create(config).start());
+ register.accept(factory.create(config).start());
} else {
throw new SecretProviderNotFoundException(SECRET_PROVIDER_NOT_FOUND_FOR_ID.formatted(id));
}
@@ -94,7 +99,7 @@ public Optional findSecretProvider(String id) {
public abstract boolean isEnabled(String pluginId);
- static class ErrorSecretProvider implements SecretProvider {
+ public static class ErrorSecretProvider implements SecretProvider {
@Override
public Maybe resolve(SecretMount secretMount) {
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcher.java b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcher.java
index 4bff56c9c..a89502f72 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcher.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcher.java
@@ -156,7 +156,7 @@ public boolean canResolveSingleValue(String location) {
* @throws IllegalArgumentException if the URL is well formatted
*/
public SecretMount toSecretMount(String location) {
- SecretURL url = SecretURL.from(location);
+ SecretURL url = SecretURL.from(location, true);
return this.findSecretProvider(url.provider())
.map(secretProvider -> {
try {
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/resolver/GraviteeConfigurationSecretPropertyResolver.java b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/resolver/GraviteeConfigurationSecretPropertyResolver.java
index ea5adf11e..2fd5f381a 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/resolver/GraviteeConfigurationSecretPropertyResolver.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/src/main/java/io/gravitee/node/secrets/service/resolver/GraviteeConfigurationSecretPropertyResolver.java
@@ -53,7 +53,7 @@ public Maybe resolve(String location) {
@Override
public boolean isWatchable(String value) {
- return SecretURL.from(value).isWatchable();
+ return SecretURL.from(value, true).isWatchable();
}
@Override
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcherTest.java b/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcherTest.java
index f72760096..5a973981f 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcherTest.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/conf/GraviteeConfigurationSecretResolverDispatcherTest.java
@@ -99,22 +99,22 @@ void should_create_secret_provider_and_watch_filtered() {
SecretMap first = cut.watch(secretMount).blockingFirst();
SecretMap last = cut.watch(secretMount).blockingLast();
assertThat(first).isNotEqualTo(last).isNotNull();
- assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null))).isPresent();
- assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null))).isNotPresent();
- assertThat(last.getSecret(new SecretMount(null, null, "created_flag", null))).isNotPresent();
- assertThat(last.getSecret(new SecretMount(null, null, "updated_flag", null))).isPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null, true))).isPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null, true))).isNotPresent();
+ assertThat(last.getSecret(new SecretMount(null, null, "created_flag", null, true))).isNotPresent();
+ assertThat(last.getSecret(new SecretMount(null, null, "updated_flag", null, true))).isPresent();
first = cut.watch(secretMount, SecretEvent.Type.UPDATED).blockingFirst();
last = cut.watch(secretMount, SecretEvent.Type.UPDATED).blockingLast();
assertThat(first).isEqualTo(last).isNotNull();
- assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null))).isNotPresent();
- assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null))).isPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null, true))).isNotPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null, true))).isPresent();
first = cut.watch(secretMount, SecretEvent.Type.CREATED).blockingFirst();
last = cut.watch(secretMount, SecretEvent.Type.CREATED).blockingLast();
assertThat(first).isEqualTo(last).isNotNull();
- assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null))).isPresent();
- assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null))).isNotPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "created_flag", null, true))).isPresent();
+ assertThat(first.getSecret(new SecretMount(null, null, "updated_flag", null, true))).isNotPresent();
Iterable all = cut.watch(secretMount, SecretEvent.Type.DELETED).blockingIterable();
assertThat(all).isEmpty();
diff --git a/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/test/TestSecretProvider.java b/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/test/TestSecretProvider.java
index f533cdf79..f1fdb7afc 100644
--- a/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/test/TestSecretProvider.java
+++ b/gravitee-node-secrets/gravitee-node-secrets-service/src/test/java/io/gravitee/node/secrets/service/test/TestSecretProvider.java
@@ -47,6 +47,6 @@ public SecretMount fromURL(SecretURL url) {
if (!url.path().equals("test")) {
throw new IllegalArgumentException();
}
- return new SecretMount(url.provider(), new SecretLocation(Map.of("path", url.path())), url.key(), url);
+ return new SecretMount(url.provider(), new SecretLocation(Map.of("path", url.path())), url.key(), url, true);
}
}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml
new file mode 100644
index 000000000..b1f15f7bf
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml
@@ -0,0 +1,147 @@
+
+
+
+ 4.0.0
+
+ io.gravitee.node
+ gravitee-node-secrets
+ 6.4.4
+
+
+ gravitee-secret-provider-mock
+ Gravitee.io - Plugin - Secret Provider - Mock
+
+
+ 3.7.1
+ 3.2.6
+ 1.7.0
+ 1.2.1
+
+ plugins/secret-providers
+
+
+
+
+ io.gravitee.node
+ gravitee-node-api
+
+
+ io.gravitee.node
+ gravitee-node-secrets-plugin-handler
+
+
+ io.reactivex.rxjava3
+ rxjava
+
+
+
+ org.assertj
+ assertj-core
+
+
+ org.springframework.security
+ spring-security-core
+ test
+
+
+ org.yaml
+ snakeyaml
+ ${snakeyaml.version}
+ test
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+
+
+ */**
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ ${maven-plugin-nexus-staging.version}
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ ${maven-plugin-gpg.version}
+
+ true
+
+
+
+ org.codehaus.mojo
+ properties-maven-plugin
+ ${maven-plugin-properties.version}
+
+
+ initialize
+ load-plugin-properties
+
+ read-project-properties
+
+
+
+ ${project.basedir}/src/main/resources/plugin.properties
+
+ false
+
+
+
+
+
+ maven-assembly-plugin
+ ${maven-plugin-assembly.version}
+
+ false
+
+ src/main/assembly/plugin-assembly.xml
+
+
+
+
+ make-resource-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/assembly/plugin-assembly.xml b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/assembly/plugin-assembly.xml
new file mode 100644
index 000000000..d1cee7e11
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/assembly/plugin-assembly.xml
@@ -0,0 +1,48 @@
+
+
+
+ plugin
+
+ zip
+
+ false
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}.jar
+
+
+
+
+
+
+ src/main/resources/schemas
+ schemas
+
+
+
+
+
+
+ lib
+ false
+
+
+
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretLocation.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretLocation.java
new file mode 100644
index 000000000..25caaf49c
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretLocation.java
@@ -0,0 +1,23 @@
+package io.gravitee.node.secrets.plugin.mock;
+
+import io.gravitee.node.api.secrets.model.SecretLocation;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record MockSecretLocation(String secret) {
+ static SecretLocation fromUrl(SecretURL url) {
+ return new MockSecretLocation(url.path()).toLocation();
+ }
+
+ static MockSecretLocation fromLocation(SecretLocation location) {
+ return new MockSecretLocation(location.get("secret"));
+ }
+
+ SecretLocation toLocation() {
+ return new SecretLocation(Map.of("secret", secret));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProvider.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProvider.java
new file mode 100644
index 000000000..bd3df1f5b
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProvider.java
@@ -0,0 +1,129 @@
+package io.gravitee.node.secrets.plugin.mock;
+
+import static io.gravitee.plugin.core.internal.AbstractPluginEventListener.SECRET_PROVIDER;
+
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.api.secrets.model.SecretEvent;
+import io.gravitee.node.api.secrets.model.SecretMap;
+import io.gravitee.node.api.secrets.model.SecretMount;
+import io.gravitee.node.api.secrets.model.SecretURL;
+import io.gravitee.node.api.secrets.util.ConfigHelper;
+import io.gravitee.node.secrets.plugin.mock.conf.ConfiguredError;
+import io.gravitee.node.secrets.plugin.mock.conf.ConfiguredEvent;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import io.gravitee.node.secrets.plugin.mock.conf.Renewal;
+import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Maybe;
+import io.reactivex.rxjava3.core.Single;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class MockSecretProvider implements SecretProvider {
+
+ public static final String PLUGIN_ID = "mock";
+
+ private final MockSecretProviderConfiguration configuration;
+
+ Map errorReturned = new ConcurrentHashMap<>();
+ Map renewalReturned = new ConcurrentHashMap<>();
+
+ @Override
+ public Maybe resolve(SecretMount secretMount) {
+ MockSecretLocation location = MockSecretLocation.fromLocation(secretMount.location());
+ log.info("{}-{} resolving secret: {}", PLUGIN_ID, SECRET_PROVIDER, location.secret());
+
+ // return error first
+ Optional errorOpt = configuration.getError(location.secret());
+ if (errorOpt.isPresent()) {
+ ConfiguredError error = errorOpt.get();
+ AtomicInteger errorReturned = this.errorReturned.computeIfAbsent(location.secret(), __ -> new AtomicInteger());
+ if (error.repeat() == 0 || error.repeat() > 0 && errorReturned.getAndIncrement() < error.repeat()) {
+ if (error.repeat() > 0 && secretMount.retryOnError()) {
+ log.info(
+ "{}-{} retrying secret: {} [{}/{}]",
+ PLUGIN_ID,
+ SECRET_PROVIDER,
+ secretMount,
+ error.repeat(),
+ errorReturned.get()
+ );
+ return resolve(secretMount);
+ }
+ String message = "fake error while getting secret [%s]: %s".formatted(location.secret(), error.message());
+ log.info("{}-{} simulating error: {}", PLUGIN_ID, SECRET_PROVIDER, message);
+ return Maybe.error(new MockSecretProviderException(message));
+ }
+ }
+
+ if (renewalReturned.containsKey(location.secret())) {
+ AtomicInteger counter = renewalReturned.get(location.secret());
+ Renewal renewal = configuration.getConfiguredRenewals().get(location.secret());
+ if (counter.get() < renewal.revisions().size()) {
+ // next revision and increment
+ return Maybe.just(getAndIncrement(renewal, counter));
+ } else if (renewal.loop()) {
+ // went over just reset
+ counter.set(0);
+ } else {
+ // went over no looping => get latest, simulate that won't change anymore
+ return Maybe.just(SecretMap.of(renewal.revisions().get(counter.get() - 1)));
+ }
+ }
+ if (configuration.getConfiguredRenewals().containsKey(location.secret())) {
+ renewalReturned.put(location.secret(), new AtomicInteger());
+ }
+
+ // standard resolution
+ Map secretMap = ConfigHelper.removePrefix(configuration.getSecrets(), location.secret());
+ if (secretMap.isEmpty()) {
+ log.info("{}-{} no secrets for: {}", PLUGIN_ID, SECRET_PROVIDER, location.secret());
+ return Maybe.empty();
+ }
+ log.info("{}-{} found secrets ({}) for: {}", PLUGIN_ID, SECRET_PROVIDER, secretMap.size(), location.secret());
+ return Maybe.just(SecretMap.of(secretMap));
+ }
+
+ private static SecretMap getAndIncrement(Renewal renewal, AtomicInteger counter) {
+ return SecretMap.of(renewal.revisions().get(counter.getAndIncrement()));
+ }
+
+ @Override
+ public Flowable watch(SecretMount secretMount) {
+ MockSecretLocation location = MockSecretLocation.fromLocation(secretMount.location());
+ List list = configuration
+ .getConfiguredEvents()
+ .stream()
+ .filter(e -> e.secret().equals(location.secret()))
+ .toList();
+ return Flowable
+ .fromIterable(list)
+ .delay(configuration.getWatchesDelayDuration(), configuration.getWatchesDelayUnit())
+ .flatMapSingle(event -> {
+ if (Objects.equals(event.error(), "null")) {
+ return Single.just(new SecretEvent(event.type(), SecretMap.of(event.data())));
+ } else {
+ return Single.error(new MockSecretProviderException(event.error()));
+ }
+ });
+ }
+
+ @Override
+ public SecretMount fromURL(SecretURL url) {
+ if (url.pluginIdMatchURLProvider() && !Objects.equals(PLUGIN_ID, url.provider())) {
+ throw new MockSecretProviderException("url does not start with '%s%s'".formatted(SecretURL.SCHEME, PLUGIN_ID));
+ }
+ return new SecretMount(url.provider(), MockSecretLocation.fromUrl(url), url.key(), url, true);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderException.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderException.java
new file mode 100644
index 000000000..5ba46e024
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderException.java
@@ -0,0 +1,12 @@
+package io.gravitee.node.secrets.plugin.mock;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class MockSecretProviderException extends RuntimeException {
+
+ public MockSecretProviderException(String message) {
+ super(message);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderFactory.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderFactory.java
new file mode 100644
index 000000000..1fd45ae89
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderFactory.java
@@ -0,0 +1,17 @@
+package io.gravitee.node.secrets.plugin.mock;
+
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.api.secrets.SecretProviderFactory;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class MockSecretProviderFactory implements SecretProviderFactory {
+
+ @Override
+ public SecretProvider create(MockSecretProviderConfiguration configuration) {
+ return new MockSecretProvider(configuration);
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredError.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredError.java
new file mode 100644
index 000000000..c8c2d3a9a
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredError.java
@@ -0,0 +1,3 @@
+package io.gravitee.node.secrets.plugin.mock.conf;
+
+public record ConfiguredError(String message, int repeat) {}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredEvent.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredEvent.java
new file mode 100644
index 000000000..fc17ee704
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredEvent.java
@@ -0,0 +1,6 @@
+package io.gravitee.node.secrets.plugin.mock.conf;
+
+import io.gravitee.node.api.secrets.model.SecretEvent;
+import java.util.Map;
+
+public record ConfiguredEvent(String secret, SecretEvent.Type type, Map data, String error) {}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/MockSecretProviderConfiguration.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/MockSecretProviderConfiguration.java
new file mode 100644
index 000000000..fcc316e6f
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/MockSecretProviderConfiguration.java
@@ -0,0 +1,110 @@
+package io.gravitee.node.secrets.plugin.mock.conf;
+
+import io.gravitee.node.api.secrets.SecretManagerConfiguration;
+import io.gravitee.node.api.secrets.model.SecretEvent;
+import io.gravitee.node.api.secrets.util.ConfigHelper;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import lombok.Getter;
+import lombok.experimental.FieldNameConstants;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Getter
+@FieldNameConstants
+public class MockSecretProviderConfiguration implements SecretManagerConfiguration {
+
+ private final boolean enabled;
+ public static final int NO_REPEAT = 0;
+
+ @Getter
+ private final Map secrets;
+
+ private final Map configuredErrors = new ConcurrentHashMap<>();
+ private final Map configuredRenewals = new ConcurrentHashMap<>();
+ private final List configuredEvents = new ArrayList<>();
+ private final Long watchesDelayDuration;
+ private final TimeUnit watchesDelayUnit;
+
+ public MockSecretProviderConfiguration(Map config) {
+ this.enabled = ConfigHelper.getProperty(config, Fields.enabled, Boolean.class, false);
+ this.secrets = ConfigHelper.removePrefix(config, "secrets");
+ Map watches = ConfigHelper.removePrefix(config, "watches");
+ watchesDelayUnit = TimeUnit.valueOf(watches.getOrDefault("delay.unit", "SECONDS").toString());
+ watchesDelayDuration = Long.parseLong(watches.getOrDefault("delay.duration", "1").toString());
+
+ // process errors
+ int i = 0;
+ String error = error(i);
+ while (config.containsKey(error + ".secret")) {
+ String secret = config.get(error + ".secret").toString();
+ int repeat = Integer.parseInt(config.getOrDefault(error + ".repeat", String.valueOf(NO_REPEAT)).toString());
+ configuredErrors.put(secret, new ConfiguredError(config.getOrDefault(error + ".message", "").toString(), repeat));
+ error = error(++i);
+ }
+
+ // process watch
+ i = 0;
+ String event = event(i);
+ while (watches.containsKey(event + ".secret")) {
+ configuredEvents.add(
+ new ConfiguredEvent(
+ watches.get(event + ".secret").toString(),
+ SecretEvent.Type.valueOf(watches.getOrDefault(event + ".type", "CREATED").toString()),
+ ConfigHelper.removePrefix(watches, event + ".data"),
+ String.valueOf(watches.get(event + ".error"))
+ )
+ );
+ event = event(++i);
+ }
+
+ // process renewal
+ i = 0;
+ String renewal = renewal(i);
+ while (config.containsKey(renewal + ".secret")) {
+ String secret = config.get(renewal + ".secret").toString();
+ boolean loop = ConfigHelper.getProperty(config, renewal + ".loop", Boolean.class, false);
+
+ List> revisions = new ArrayList<>();
+ int r = 0;
+ Map data = ConfigHelper.removePrefix(config, revision(renewal, r) + ".data");
+ while (!data.isEmpty()) {
+ revisions.add(data);
+ data = ConfigHelper.removePrefix(config, revision(renewal, ++r) + ".data");
+ }
+ configuredRenewals.put(secret, new Renewal(loop, revisions));
+ renewal = renewal(++i);
+ }
+ }
+
+ private static String event(int i) {
+ return "events[%s]".formatted(i);
+ }
+
+ private static String error(int i) {
+ return "errors[%s]".formatted(i);
+ }
+
+ private static String renewal(int i) {
+ return "renewals[%s]".formatted(i);
+ }
+
+ private static String revision(String base, int j) {
+ return "%s.revisions[%s]".formatted(base, j);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public Optional getError(String secret) {
+ return Optional.ofNullable(configuredErrors.get(secret));
+ }
+}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/Renewal.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/Renewal.java
new file mode 100644
index 000000000..8f4290c15
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/Renewal.java
@@ -0,0 +1,10 @@
+package io.gravitee.node.secrets.plugin.mock.conf;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public record Renewal(boolean loop, List> revisions) {}
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/resources/plugin.properties b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/resources/plugin.properties
new file mode 100644
index 000000000..ed1ba0e83
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/resources/plugin.properties
@@ -0,0 +1,7 @@
+id=mock
+name=Mock Secret Provider
+version=${project.version}
+description=Mock secret provider allow to get secrets from configuration for testing purposes
+class=io.gravitee.node.secrets.plugin.mock.MockSecretProviderFactory
+type=secret-provider
+
diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/test/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderTest.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/test/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderTest.java
new file mode 100644
index 000000000..7559503ad
--- /dev/null
+++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/test/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderTest.java
@@ -0,0 +1,197 @@
+package io.gravitee.node.secrets.plugin.mock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.gravitee.node.api.secrets.SecretProvider;
+import io.gravitee.node.api.secrets.model.*;
+import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.security.util.InMemoryResource;
+
+/**
+ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class MockSecretProviderTest {
+
+ private SecretProvider cut;
+
+ @BeforeEach
+ void setup() {
+ InMemoryResource inMemoryResource = new InMemoryResource(
+ """
+ enabled: true
+ secrets:
+ redis:
+ password: r3d1s
+ ldap:
+ password: 1da9
+ flaky:
+ value: now it works
+ retry-test:
+ value: after several retries it works
+ loop:
+ value: loop 1
+ renewable:
+ value: once
+ errors:
+ - secret: flaky
+ message: next attempt it should work
+ repeat: 1
+ - secret: kafka
+ message: that's just ain't working
+ - secret: retry-test
+ message: fatal error
+ repeat: 10
+ delayMs: 200
+ renewals:
+ - secret: loop
+ loop: true
+ revisions:
+ - data:
+ value: loop 2
+ - data:
+ value: loop 3
+ - secret: renewable
+ revisions:
+ - data:
+ value: twice and no more
+ watches:
+ delay:
+ unit: SECONDS
+ duration: 2
+ events:
+ - secret: apikeys
+ data:
+ partner1: "123"
+ partner2: "456"
+ type: CREATED
+ - secret: apikeys
+ data:
+ partner1: "789"
+ partner2: "101112"
+ type: UPDATED
+ - secret: apikeys
+ data: {}
+ error: odd enough message to be unique
+ """
+ );
+ final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+ yaml.setResources(inMemoryResource);
+ Map conf = new LinkedHashMap<>(yaml.getObject());
+ this.cut = new MockSecretProviderFactory().create(new MockSecretProviderConfiguration(conf));
+ }
+
+ @Test
+ void should_create_mount() {
+ SecretMount secretMountRedis = cut.fromURL(SecretURL.from("secret://mock/redis:password"));
+ assertThat(secretMountRedis.provider()).isEqualTo("mock");
+ assertThat((String) secretMountRedis.location().get("secret")).isEqualTo("redis");
+ assertThat(secretMountRedis.key()).isEqualTo("password");
+ }
+
+ @Test
+ void should_resolve() {
+ SecretMount secretMountRedis = cut.fromURL(SecretURL.from("secret://mock/redis:password"));
+ cut.resolve(secretMountRedis).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("password", "r3d1s")));
+
+ SecretMount secretMountLdap = cut.fromURL(SecretURL.from("secret://mock/ldap"));
+ cut.resolve(secretMountLdap).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("password", "1da9")));
+ }
+
+ @Test
+ void should_return_empty() {
+ SecretMount secretMountEmpty = cut.fromURL(SecretURL.from("secret://mock/empty:password"));
+ cut.resolve(secretMountEmpty).test().awaitDone(100, TimeUnit.MILLISECONDS).assertNoErrors().assertComplete();
+ }
+
+ @Test
+ void should_return_an_error() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/kafka"));
+ cut
+ .resolve(secretMount)
+ .test()
+ .awaitDone(100, TimeUnit.MILLISECONDS)
+ .assertError(err -> err.getMessage().contains("that's just ain't working"));
+ }
+
+ @Test
+ void should_return_an_error_then_work() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/flaky")).withoutRetries();
+ cut
+ .resolve(secretMount)
+ .test()
+ .awaitDone(100, TimeUnit.MILLISECONDS)
+ .assertError(err -> err.getMessage().contains("next attempt it should work"));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "now it works")));
+ }
+
+ @Test
+ void should_return_a_secret_after_several_200ms_retries() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/retry-test"));
+ cut
+ .resolve(secretMount)
+ .test()
+ .awaitDone(6, TimeUnit.SECONDS)
+ .assertValue(SecretMap.of(Map.of("value", "after several retries it works")));
+ }
+
+ @Test
+ void should_watch_values() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/apikeys"));
+ cut
+ .watch(secretMount)
+ .test()
+ .awaitDone(3, TimeUnit.SECONDS)
+ .assertValueAt(
+ 0,
+ secretEvent ->
+ secretEvent.type() == SecretEvent.Type.CREATED &&
+ secretEvent.secretMap().asMap().values().containsAll(List.of(new Secret("123", false), new Secret("456", false)))
+ )
+ .assertValueAt(
+ 1,
+ secretEvent ->
+ secretEvent.type() == SecretEvent.Type.UPDATED &&
+ secretEvent.secretMap().asMap().values().containsAll(List.of(new Secret("789", false), new Secret("101112", false)))
+ )
+ .assertError(err -> err.getMessage().contains("odd enough message to be unique"));
+ }
+
+ @Test
+ void should_renew_once() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/renewable"));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "once")));
+ cut
+ .resolve(secretMount)
+ .test()
+ .awaitDone(100, TimeUnit.MILLISECONDS)
+ .assertValue(SecretMap.of(Map.of("value", "twice and no more")));
+ cut
+ .resolve(secretMount)
+ .test()
+ .awaitDone(100, TimeUnit.MILLISECONDS)
+ .assertValue(SecretMap.of(Map.of("value", "twice and no more")));
+ }
+
+ @Test
+ void should_renew_in_loop() {
+ SecretMount secretMount = cut.fromURL(SecretURL.from("secret://mock/loop"));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 1")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 2")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 3")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 1")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 2")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 3")));
+ cut.resolve(secretMount).test().awaitDone(100, TimeUnit.MILLISECONDS).assertValue(SecretMap.of(Map.of("value", "loop 1")));
+ }
+}
diff --git a/gravitee-node-secrets/pom.xml b/gravitee-node-secrets/pom.xml
index 1094b833d..2fca06b06 100644
--- a/gravitee-node-secrets/pom.xml
+++ b/gravitee-node-secrets/pom.xml
@@ -34,6 +34,8 @@
gravitee-node-secrets-plugin-handler
gravitee-node-secrets-service
+ gravitee-node-secrets-runtime
+ gravitee-secret-provider-mock
@@ -51,4 +53,12 @@
+
+
+ com.esotericsoftware
+ kryo
+ 5.6.0
+
+
+
diff --git a/pom.xml b/pom.xml
index 8843c50a0..956499994 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
8.1.0
- 3.3.3
+ 4.5.1
4.1.0
1.25.0
1.0.0