diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/license/License.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/license/License.java index 9a633b373..f12594926 100644 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/license/License.java +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/license/License.java @@ -65,23 +65,23 @@ public interface License { boolean isFeatureEnabled(String feature); /** - * Verify that the license is valid. This checks both the signature and the expiration date. + * Verify that the license is valid. This checks both the signature and the pollInterval date. * * @throws InvalidLicenseException if the license is expired or invalid. */ void verify() throws InvalidLicenseException; /** - * Return the expiration date of the license or null if the license has no expiration date. + * Return the pollInterval date of the license or null if the license has no pollInterval date. * - * @return the license expiration date. + * @return the license pollInterval date. */ @Nullable Date getExpirationDate(); /** * Indicates if the license is expired or not. - * Having a null expiration date means no expiration. + * Having a null pollInterval date means no pollInterval. * * @return true if the license has expired, false else. */ diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/Secret.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/Secret.java index d3c529f2b..d479fb771 100644 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/Secret.java +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/Secret.java @@ -20,6 +20,10 @@ public final class Secret { private final Object data; private final boolean base64Encoded; + public Secret() { + this(""); + } + /** * Builds a secret value assuming it is not base64 encoded * diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretLocation.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretLocation.java index 94586f3ea..cd05ce283 100644 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretLocation.java +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretLocation.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import lombok.ToString; /** * This class represents where the secret is from a provider perspective. It is a map internally. @@ -10,6 +11,7 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ +@ToString public class SecretLocation { private final Map 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_TYPEis 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 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 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