From ba407e8bf96f7921c70295343dab85e27677e677 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Thu, 26 Sep 2024 17:22:54 +0200 Subject: [PATCH 01/15] feat: rtsec step1 --- .../node/api/secrets/model/Secret.java | 4 + .../node/api/secrets/model/SecretMap.java | 8 + .../node/api/secrets/model/SecretMount.java | 6 +- .../node/api/secrets/model/SecretURL.java | 16 +- .../runtime/RuntimeSecretException.java | 22 ++ .../secrets/runtime/discovery/Definition.java | 5 + .../runtime/discovery/DefinitionBrowser.java | 15 ++ .../discovery/DefinitionPayloadNotifier.java | 9 + .../runtime/discovery/DiscoveryContext.java | 9 + .../runtime/discovery/DiscoveryLocation.java | 9 + .../runtime/discovery/PayloadLocation.java | 6 + .../api/secrets/runtime/discovery/Ref.java | 64 ++++++ .../secrets/runtime/grant/GrantService.java | 18 ++ .../runtime/providers/ResolverService.java | 16 ++ .../providers/SecretProviderDeployer.java | 11 + .../node/api/secrets/runtime/spec/ACLs.java | 13 ++ .../secrets/runtime/spec/RenewalPolicy.java | 15 ++ .../node/api/secrets/runtime/spec/Spec.java | 57 +++++ .../runtime/spec/SpecLifecycleService.java | 16 ++ .../api/secrets/runtime/storage/Cache.java | 25 ++ .../api/secrets/runtime/storage/Entry.java | 21 ++ .../node/api/secrets/model/SecretMapTest.java | 6 +- .../api/secrets/model/SecretMountTest.java | 7 +- .../node/api/secrets/model/SecretURLTest.java | 14 +- .../internal/test/TestSecretProvider.java | 2 +- .../gravitee-node-secrets-runtime/pom.xml | 59 +++++ .../RuntimeSecretProcessingService.java | 117 ++++++++++ .../runtimesecrets/config/Config.java | 10 + .../discovery/ContextRegistry.java | 53 +++++ .../discovery/DefinitionBrowserRegistry.java | 29 +++ .../discovery/PayloadRefParser.java | 80 +++++++ .../runtimesecrets/discovery/RefParser.java | 213 ++++++++++++++++++ .../runtimesecrets/el/ContextUpdater.java | 25 ++ .../services/runtimesecrets/el/Formatter.java | 91 ++++++++ .../services/runtimesecrets/el/Result.java | 16 ++ .../services/runtimesecrets/el/Service.java | 138 ++++++++++++ .../errors/SecretAccessDeniedException.java | 14 ++ .../errors/SecretEmptyException.java | 14 ++ .../errors/SecretKeyNotFoundException.java | 14 ++ .../errors/SecretNotFoundException.java | 14 ++ .../errors/SecretProviderException.java | 14 ++ .../SecretProviderNotFoundException.java | 14 ++ .../errors/SecretRefParsingException.java | 14 ++ .../errors/SecretSpecNotFoundException.java | 14 ++ .../grant/DefaultGrantService.java | 111 +++++++++ .../runtimesecrets/grant/GrantRegistry.java | 30 +++ .../providers/DefaultRuntimeResolver.java | 35 +++ ...omConfigurationSecretProviderDeployer.java | 35 +++ .../providers/SecretProviderRegistry.java | 49 ++++ .../spec/DefaultSpecLifecycleService.java | 84 +++++++ .../spec/registry/EnvAwareSpecRegistry.java | 51 +++++ .../spec/registry/SpecRegistry.java | 91 ++++++++ .../runtimesecrets/spring/BeanFactory.java | 116 ++++++++++ .../storage/SimpleOffHeapCache.java | 92 ++++++++ .../runtimesecrets/api/model/RefTest.java | 128 +++++++++++ .../discovery/RefDiscovererTest.java | 88 ++++++++ .../grant/DefaultGrantServiceTest.java | 134 +++++++++++ .../storage/SimpleOffHeapCacheTest.java | 104 +++++++++ ...ConfigurationSecretResolverDispatcher.java | 2 +- ...eeConfigurationSecretPropertyResolver.java | 2 +- ...igurationSecretResolverDispatcherTest.java | 16 +- .../service/test/TestSecretProvider.java | 2 +- gravitee-node-secrets/pom.xml | 9 + 63 files changed, 2483 insertions(+), 33 deletions(-) create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/RuntimeSecretException.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Definition.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionBrowser.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionPayloadNotifier.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryContext.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DiscoveryLocation.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/PayloadLocation.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/SecretProviderDeployer.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ACLs.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/RenewalPolicy.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/SpecLifecycleService.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Cache.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Entry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Config.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefinitionBrowserRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/RefParser.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Result.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Service.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretEmptyException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretKeyNotFoundException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretNotFoundException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderNotFoundException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretRefParsingException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretSpecNotFoundException.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/GrantRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/SecretProviderRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCache.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefDiscovererTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantServiceTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCacheTest.java 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/SecretMap.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java index 92969d0ff..166a0a925 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 @@ -45,6 +45,14 @@ public SecretMap(Map map, Instant expireAt) { 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 * 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..d1d2fd2c4 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 withRetries) { /** * 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..4173d7c7e 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; @@ -27,7 +30,7 @@ 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 +38,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); 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/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..1a7e374ef --- /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(T definition); + + Definition getDefinitionKindLocation(T definition, Map metadata); + + void findPayloads(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..3a4ac4646 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionPayloadNotifier.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 interface DefinitionPayloadNotifier { + void onPayload(String payload, PayloadLocation location); +} 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..064913f48 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java @@ -0,0 +1,64 @@ +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 toRuntimeSpec(String envId) { + return new Spec(null, null, mainExpression.value(), secondaryExpression().value(), null, false, 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/GrantService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java new file mode 100644 index 000000000..6dcba6514 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/GrantService.java @@ -0,0 +1,18 @@ +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; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public interface GrantService { + boolean isGranted(String token); + + boolean authorize(DiscoveryContext context, Spec spec); + + void grant(DiscoveryContext context); + + 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..56305d41c --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java @@ -0,0 +1,16 @@ +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.storage.Entry; +import io.reactivex.rxjava3.core.Single; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public interface ResolverService { + Single resolve(String envId, SecretMount secretMount); + + SecretMount 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..8da82de32 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/SecretProviderDeployer.java @@ -0,0 +1,11 @@ +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 { + void deploy(String id, String pluginId, Map config, 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..84d777605 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ACLs.java @@ -0,0 +1,13 @@ +package io.gravitee.node.api.secrets.runtime.spec; + +import java.util.List; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ + +public record ACLs(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/RenewalPolicy.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/RenewalPolicy.java new file mode 100644 index 000000000..319f9e035 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/RenewalPolicy.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 RenewalPolicy(Type type, Duration duration, Duration checkBeforeTTL) { + public enum Type { + NONE, + TTL, + POLL, + } +} 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..5f84bf879 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java @@ -0,0 +1,57 @@ +package io.gravitee.node.api.secrets.runtime.spec; + +import static io.gravitee.node.api.secrets.runtime.discovery.Ref.URI_KEY_SEPARATOR; + +import io.gravitee.node.api.secrets.model.SecretURL; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @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 isRuntime, + RenewalPolicy renewalPolicy, + 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 uri + URI_KEY_SEPARATOR + key; + } + + public SecretURL toSecretURL() { + return SecretURL.from(uriAndKey(), false); + } + + public String naturalId() { + return name != null && !name.isEmpty() ? name : uri; + } + + public record ChildSpec(String name, String uri, String key) {} +} 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..ca0d9cd5e --- /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); + + 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..305694b39 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/Cache.java @@ -0,0 +1,25 @@ +package io.gravitee.node.api.secrets.runtime.storage; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public interface Cache { + CacheKey put(String envId, String naturalId, Entry value); + + Optional get(String envId, String naturalId); + + void computeIfAbsent(String envId, String naturalId, Supplier supplier); + + void evict(String envId, String naturalId); + + record CacheKey(String envId, String naturalId) { + @Override + public String toString() { + return envId + "-" + naturalId; + } + } +} 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-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..c411edeef --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + io.gravitee.node + gravitee-node-secrets + 6.4.2 + + + gravitee-node-secrets-runtime + Gravitee.io - Node - Secrets - Runtime + + + + org.springframework + spring-core + + + io.gravitee.node + gravitee-node-api + + + io.reactivex.rxjava3 + rxjava + + + io.gravitee.el + gravitee-expression-language + 3.1.0 + compile + + + + org.awaitility + awaitility + test + + + + diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java new file mode 100644 index 000000000..bc86cce51 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java @@ -0,0 +1,117 @@ +package com.graviteesource.services.runtimesecrets; + +import com.graviteesource.services.runtimesecrets.discovery.ContextRegistry; +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.registry.EnvAwareSpecRegistry; +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 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 RuntimeSecretProcessingService { + + private final DefinitionBrowserRegistry definitionBrowserRegistry; + private final ContextRegistry contextRegistry; + private final GrantService grantService; + private final SpecLifecycleService specLifecycleService; + private final EnvAwareSpecRegistry specRegistry; + + /** + *
  • finds a {@link DefinitionBrowser}
  • + *
  • Run it to get {@link DiscoveryContext}
  • + *
  • Inject EL {@link PayloadRefParser}
  • + *
  • Find {@link Spec}
  • + *
  • Grant {@link DiscoveryContext}
  • + * @param definition the secret naturalId container + * @param metadata some optional metadata + * @param the kind of subject + */ + void processSecrets(String envId, @Nonnull T definition, @Nullable Map metadata) { + Optional> browser = definitionBrowserRegistry.findBrowser(definition); + if (browser.isEmpty()) { + log.info("No definition browser found for kind [{}]", definition.getClass()); + return; + } + + DefinitionBrowser definitionBrowser = browser.get(); + Definition rootDefinition = definitionBrowser.getDefinitionKindLocation(definition, metadata); + DefaultPayloadNotifier notifier = new DefaultPayloadNotifier(rootDefinition, envId); + definitionBrowser.findPayloads(notifier); + + // register contexts by naturalId 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()); + } + boolean granted = grantService.authorize(context, spec); + if (granted && context.ref().mainExpression().isLiteral()) { + grantService.grant(context); + } + } + } + + static class DefaultPayloadNotifier implements DefinitionPayloadNotifier { + + @Getter + final List contextList = new ArrayList<>(); + + final Definition rootDefinition; + final String envId; + + DefaultPayloadNotifier(Definition rootDefinition, String envId) { + this.rootDefinition = rootDefinition; + this.envId = envId; + } + + @Override + public void onPayload(String payload, PayloadLocation payloadLocation) { + 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); + } + } +} 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..c0a87ac24 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Config.java @@ -0,0 +1,10 @@ +package com.graviteesource.services.runtimesecrets.config; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public record Config(boolean allowOnTheFlySpecs, boolean allowEmptyACLSpecs) { + public static final String ALLOW_EMPTY_ACL_SPECS = "api.secrets.allowEmptyNoACLsSpecs"; + public static final String ALLOW_ON_THE_FLY_SPECS = "api.secrets.allowOnTheFlySpecs"; +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java new file mode 100644 index 000000000..8ec071864 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java @@ -0,0 +1,53 @@ +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.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.List; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class 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()) { + byName.put(context.ref().mainExpression().value(), context); + } + if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { + byUri.put(context.ref().mainExpression().value(), context); + if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { + byUriAndKey.put(context.ref().uriAndKey(), context); + } + } + byDefinitionSpec.put(definition, context); + } + + public List findBySpec(Spec spec) { + List result = new ArrayList<>(); + if (spec.name() != null && !spec.name().isEmpty()) { + result.addAll(byName.get(spec.name())); + } + if (spec.uri() != null && !spec.uri().isEmpty()) { + result.addAll(byUri.get(spec.name())); + } + if (spec.key() != null && !spec.key().isEmpty()) { + result.addAll(byUriAndKey.get(spec.uriAndKey())); + } + return result; + } + + public List getByDefinition(Definition definition) { + return (List) byDefinitionSpec.get(definition); + } +} 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..d66827147 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefinitionBrowserRegistry.java @@ -0,0 +1,29 @@ +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..b8c6c6de3 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java @@ -0,0 +1,80 @@ +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("naturalId and replacement list don't match in size"); + } + + for (int i = 0; i < rawRefs.size(); i++) { + String replacement = expressions.get(i); + + // replace naturalId 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 naturalId positions + for (int p = i + 1; p < expressions.size(); p++) { + rawRefs.get(p).position.move(lengthDiff); + } + } + + 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..cc945c56d --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/RefParser.java @@ -0,0 +1,213 @@ +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 naturalId (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("naturalId 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.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 starting the 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 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 looks like nothing is specified"); + } + + 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, 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/ContextUpdater.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java new file mode 100644 index 000000000..cee1d535d --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java @@ -0,0 +1,25 @@ +package com.graviteesource.services.runtimesecrets.el; + +import com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; +import io.gravitee.el.TemplateContext; +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 +public class ContextUpdater { + + private final Cache cache; + private final GrantService grantService; + private final SpecLifecycleService specLifecycleService; + private final EnvAwareSpecRegistry specRegistry; + + public void addRuntimeSecretsService(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/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..dea549c3b --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java @@ -0,0 +1,91 @@ +package com.graviteesource.services.runtimesecrets.el; + +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', '%s', '%s', %s)}"; + 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; + switch (context.ref().secondaryType()) { + case KEY -> el = fromGrant(context.id(), envId, mainSpec, context.ref().secondaryExpression()); + 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())); + } + } + 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, String envId, String expression, Ref.Expression keySpec) { + return FROM_GRANT_TEMPLATE.formatted(id, envId, expression, quoteLiteral(keySpec)); + } + + 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..be7c59635 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Result.java @@ -0,0 +1,16 @@ +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..161427246 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Service.java @@ -0,0 +1,138 @@ +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.registry.EnvAwareSpecRegistry; +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.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.Entry; +import java.util.Map; +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 EnvAwareSpecRegistry specRegistry; + + public String fromGrant(String token, String envId, String expression, String key) { + boolean granted = grantService.isGranted(token); + if (!granted) { + return resultToValue(new Result(Result.Type.DENIED, "secret [%s] is denied in environment [%s]".formatted(expression, envId))); + } + return resultToValue( + toResult( + cache + .get(envId, expression) + .orElse( + new Entry(Entry.Type.EMPTY, null, "no value in cache for [%s] in environment [%s]".formatted(expression, 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, uriWithKey.length()); + Ref ref = uriAndKey.asRef(); + Spec spec = specRegistry.getFromUriAndKey(envId, uriWithKey); + if (spec == null && specLifecycleService.shouldDeployOnTheFly(ref)) { + spec = specLifecycleService.deployOnTheFly(envId, ref); + } + return grantAndGet(envId, definitionKind, definitionId, spec, ref, uriAndKey.uri(), 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, name, spec.key()); + } + + private String grantAndGet(String envId, String definitionKind, String definitionId, Spec spec, Ref ref, String naturalId, String key) { + boolean granted = grantService.authorize( + 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(naturalId, envId))); + } + return resultToValue( + toResult( + cache + .get(envId, naturalId) + .orElse( + new Entry(Entry.Type.EMPTY, null, "no value in cache for [%s] in environment [%s]".formatted(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); + if (secret != null) { + result = new Result(Result.Type.VALUE, secret.asString()); + } else { + result = new Result(Result.Type.KEY_NOT_FOUND, "key [%s] not found"); + } + } + 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/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..1141a5dad --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java @@ -0,0 +1,14 @@ +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..a33bb6d63 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretEmptyException.java @@ -0,0 +1,14 @@ +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..f2fc6f4e8 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretKeyNotFoundException.java @@ -0,0 +1,14 @@ +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..27cd15442 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretNotFoundException.java @@ -0,0 +1,14 @@ +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..ad9af9a76 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderException.java @@ -0,0 +1,14 @@ +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..2f060cd7a --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretProviderNotFoundException.java @@ -0,0 +1,14 @@ +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..12d0aacde --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretRefParsingException.java @@ -0,0 +1,14 @@ +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..7370d3886 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretSpecNotFoundException.java @@ -0,0 +1,14 @@ +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..84c8833c0 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java @@ -0,0 +1,111 @@ +package com.graviteesource.services.runtimesecrets.grant; + +import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_ACL_SPECS; +import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_ON_THE_FLY_SPECS; +import static io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation.PLUGIN_KIND; + +import com.graviteesource.services.runtimesecrets.config.Config; +import com.graviteesource.services.runtimesecrets.errors.SecretSpecNotFoundException; +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.grant.GrantService; +import io.gravitee.node.api.secrets.runtime.spec.ACLs; +import io.gravitee.node.api.secrets.runtime.spec.Spec; +import java.util.Arrays; +import java.util.Objects; +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 authorize(@Nonnull DiscoveryContext context, Spec spec) { + if (spec == null) { + throw new SecretSpecNotFoundException( + "no spec found or created on-the-fly for ref [%s] in envId [%s], %s=%s".formatted( + context.ref().rawRef(), + context.envId(), + ALLOW_ON_THE_FLY_SPECS, + config.allowOnTheFlySpecs() + ) + ); + } + if (spec.acls() == null) { + if (!config.allowEmptyACLSpecs()) { + log.warn( + "secret spec for ref [{}] is not granted because is does not contains ACLs and this is not allowed. see: {}", + context.ref().rawRef(), + ALLOW_EMPTY_ACL_SPECS + ); + return false; + } else { + return Objects.equals(context.envId(), spec.envId()); + } + } + + return checkACLs(spec, context); + } + + public boolean isGranted(@Nonnull String token) { + return grantRegistry.exists(token); + } + + @Override + public void grant(@Nonnull DiscoveryContext context) { + grantRegistry.register(context); + } + + @Override + public void revoke(@Nonnull DiscoveryContext context) { + grantRegistry.unregister(context); + } + + private boolean checkACLs(Spec spec, DiscoveryContext context) { + Predicate noDefKind = acls -> + acls.definitions() == null || + acls.definitions().isEmpty() || + acls.definitions().stream().allMatch(def -> def.kind() == null || def.kind().isEmpty()); + + Predicate defKind = 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 defId = acls -> + acls.definitions().stream().anyMatch(defACLs -> defACLs.ids().contains(context.location().definition().id())); + + Predicate noPlugin = acls -> acls.plugins() == null || acls.plugins().isEmpty(); + + Predicate plugin = 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(defKind).and(noDefId.or(defId)).and(noPlugin.or(plugin)).test(spec.acls()); + } +} 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..fcab420a7 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/GrantRegistry.java @@ -0,0 +1,30 @@ +package com.graviteesource.services.runtimesecrets.grant; + +import io.gravitee.node.api.secrets.runtime.discovery.DiscoveryContext; +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(DiscoveryContext context) { + grants.put(context.id().toString(), null); + } + + public void unregister(DiscoveryContext... contexts) { + if (contexts != null) { + Arrays.stream(contexts).map(DiscoveryContext::id).map(UUID::toString).forEach(grants::remove); + } + } + + public boolean exists(String token) { + return grants.containsKey(token); + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java new file mode 100644 index 000000000..db3ba255a --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java @@ -0,0 +1,35 @@ +package com.graviteesource.services.runtimesecrets.providers; + +import io.gravitee.node.api.secrets.SecretProvider; +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.storage.Entry; +import io.reactivex.rxjava3.core.Single; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class DefaultRuntimeResolver implements ResolverService { + + private final SecretProviderRegistry secretProviderRegistry; + + public DefaultRuntimeResolver(SecretProviderRegistry secretProviderRegistry) { + this.secretProviderRegistry = secretProviderRegistry; + } + + @Override + public Single resolve(String envId, SecretMount secretMount) { + SecretProvider secretProvider = secretProviderRegistry.get(envId, secretMount.provider()); + return secretProvider + .resolve(secretMount) + .map(secretMap -> new Entry(Entry.Type.VALUE, secretMap.asMap(), null)) + .defaultIfEmpty(new Entry(Entry.Type.NOT_FOUND, null, null)); + } + + @Override + public SecretMount toSecretMount(String envId, SecretURL secretURL) { + return secretProviderRegistry.get(envId, secretURL.provider()).fromURL(secretURL); + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java new file mode 100644 index 000000000..b08d34155 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java @@ -0,0 +1,35 @@ +package com.graviteesource.services.runtimesecrets.providers; + +import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer; +import java.util.Map; +import org.springframework.core.env.Environment; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class FromConfigurationSecretProviderDeployer implements SecretProviderDeployer { + + private boolean init; + private final Environment environment; + + public FromConfigurationSecretProviderDeployer(Environment environment) { + this.environment = environment; + } + + public void init() { + if (!init) { + doInit(); + init = true; + } + } + + private void doInit() { + // TODO + } + + @Override + public void deploy(String id, String pluginId, Map config, String envId) { + // TODO + } +} 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..a309aa278 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/SecretProviderRegistry.java @@ -0,0 +1,49 @@ +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 java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class SecretProviderRegistry { + + Multimap perEnv = MultimapBuilder.hashKeys().arrayListValues().build(); + Map allEnvs = new HashMap<>(); + + public void register(String id, SecretProvider provider, String envId) { + if (envId == null || envId.isEmpty()) { + allEnvs.put(id, provider); + } else { + 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 SecretProvider get(String envId, String id) { + return perEnv + .get(envId) + .stream() + .filter(entry -> entry.id().equals(id)) + .map(SecretProviderEntry::provider) + .findFirst() + .or(() -> Optional.ofNullable(allEnvs.get(id))) + .orElseThrow(() -> + new SecretProviderNotFoundException("Cannot find secret provider with id [%s] for environmentID [%s]".formatted(id, envId)) + ); + } + + public record SecretProviderEntry(String id, SecretProvider provider) {} +} 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..3ea641241 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java @@ -0,0 +1,84 @@ +package com.graviteesource.services.runtimesecrets.spec; + +import com.graviteesource.services.runtimesecrets.config.Config; +import com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; +import io.gravitee.node.api.secrets.model.SecretMount; +import io.gravitee.node.api.secrets.model.SecretURL; +import io.gravitee.node.api.secrets.runtime.discovery.Ref; +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.Entry; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import lombok.RequiredArgsConstructor; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +@RequiredArgsConstructor +public class DefaultSpecLifecycleService implements SpecLifecycleService { + + private final EnvAwareSpecRegistry specRegistry; + private final Cache cache; + private final ResolverService resolverService; + private final Config config; + + @Override + public boolean shouldDeployOnTheFly(Ref ref) { + return (ref.mainType() == Ref.MainType.URI && ref.mainExpression().isLiteral() && config.allowOnTheFlySpecs()); + } + + @Override + public Spec deployOnTheFly(String envId, Ref ref) { + Spec runtimeSpec = ref.toRuntimeSpec(envId); + cache.computeIfAbsent( + envId, + runtimeSpec.naturalId(), + () -> { + specRegistry.register(envId, runtimeSpec); + SecretURL secretURL = runtimeSpec.toSecretURL(); + SecretMount mount = resolverService.toSecretMount(envId, secretURL).withoutRetries(); + return resolverService + .resolve(envId, mount) + .subscribeOn(Schedulers.io()) + .onErrorResumeNext(t -> { + Entry entry = new Entry(Entry.Type.ERROR, null, t.getMessage()); + asyncResolution(runtimeSpec); + return Single.just(entry); + }) + .blockingGet(); + } + ); + + return runtimeSpec; + } + + private Disposable asyncResolution(Spec spec) { + SecretURL secretURL = spec.toSecretURL(); + String envId = spec.envId(); + SecretMount mount = resolverService.toSecretMount(envId, secretURL); + return resolverService + .resolve(envId, mount) + .subscribeOn(Schedulers.io()) + .onErrorResumeNext(t -> Single.just(new Entry(Entry.Type.ERROR, null, t.getMessage()))) + .subscribe(entry -> cache.put(envId, spec.naturalId(), entry)); + } + + @Override + public void deploy(Spec spec) { + specRegistry.register(spec.envId(), spec); + // TODO check diff + // TODO if change clean by old name or uri + Disposable disposable = asyncResolution(spec/*, cleanupLambda*/); + } + + @Override + public void undeploy(Spec spec) { + specRegistry.unregister(spec.envId(), spec); + cache.evict(spec.envId(), spec.naturalId()); + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java new file mode 100644 index 000000000..d56004af9 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java @@ -0,0 +1,51 @@ +package com.graviteesource.services.runtimesecrets.spec.registry; + +import io.gravitee.node.api.secrets.runtime.discovery.Ref; +import io.gravitee.node.api.secrets.runtime.spec.Spec; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class EnvAwareSpecRegistry { + + private final Map registries = new HashMap<>(); + + public void register(String envId, Spec spec) { + registry(envId).register(spec); + } + + public void unregister(String envId, Spec spec) { + registry(envId).unregister(spec); + } + + public Spec getFromName(String envId, String name) { + return registry(envId).getFromName(name); + } + + public Spec getFromUri(String envId, String uri) { + return registry(envId).getFromUri(uri); + } + + public Spec getFromUriAndKey(String envId, String uriAndKey) { + return registry(envId).getFromUriAndKey(uriAndKey); + } + + public Spec getFromID(String envId, String id) { + return registry(envId).getFromID(id); + } + + public Spec fromSpec(String envId, Spec query) { + return registry(envId).fromSpec(query); + } + + public Spec fromRef(String envId, Ref query) { + return registry(envId).fromRef(query); + } + + private SpecRegistry registry(String envId) { + return registries.computeIfAbsent(envId, ignore -> new SpecRegistry()); + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java new file mode 100644 index 000000000..0eb4bfe87 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java @@ -0,0 +1,91 @@ +package com.graviteesource.services.runtimesecrets.spec.registry; + +import io.gravitee.node.api.secrets.runtime.discovery.Ref; +import io.gravitee.node.api.secrets.runtime.spec.Spec; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +class SpecRegistry { + + private final Map byName = new HashMap<>(); + private final Map byUri = new HashMap<>(); + private final Map byUriAndKey = new HashMap<>(); + private final Map byID = new HashMap<>(); + + 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) { + byUri.put(spec.uri(), spec); + if (spec.key() != null) { + byUriAndKey.put(spec.uriAndKey(), spec); + } + } + } + + void unregister(Spec spec) { + if (spec.id() != null) { + byID.remove(spec.id()); + } + if (spec.uri() != null) { + byUri.remove(spec.uri()); + if (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 getFromUri(String uri) { + return byUri.get(uri); + } + + Spec getFromUriAndKey(String uriAndKey) { + return byUriAndKey.get(uriAndKey); + } + + Spec getFromID(String id) { + return byID.get(id); + } + + 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.mainExpression().value()); + } + return byUri.get(query.mainExpression().value()); + } + return null; + } + + Spec fromSpec(Spec query) { + if (query.id() != null) { + return byID.get(query.id()); + } else if (query.name() != null) { + return byName.get(query.name()); + } else if (query.uri() != null) { + if (query.key() != null) { + return byUriAndKey.get(query.uriAndKey()); + } + return byUri.get(query.uri()); + } + return null; + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java new file mode 100644 index 000000000..63ed829ee --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java @@ -0,0 +1,116 @@ +package com.graviteesource.services.runtimesecrets.spring; + +import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_ACL_SPECS; +import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_ON_THE_FLY_SPECS; + +import com.graviteesource.services.runtimesecrets.RuntimeSecretProcessingService; +import com.graviteesource.services.runtimesecrets.config.Config; +import com.graviteesource.services.runtimesecrets.discovery.ContextRegistry; +import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; +import com.graviteesource.services.runtimesecrets.el.ContextUpdater; +import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService; +import com.graviteesource.services.runtimesecrets.grant.GrantRegistry; +import com.graviteesource.services.runtimesecrets.providers.DefaultRuntimeResolver; +import com.graviteesource.services.runtimesecrets.providers.FromConfigurationSecretProviderDeployer; +import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry; +import com.graviteesource.services.runtimesecrets.spec.DefaultSpecLifecycleService; +import com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; +import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache; +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 java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; +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 BeanFactory { + + @Bean + Config config( + @Value("${" + ALLOW_ON_THE_FLY_SPECS + ":true}") boolean allowRuntimeSpecs, + @Value("${" + ALLOW_EMPTY_ACL_SPECS + ":true}") boolean allowEmptyACLSpecs + ) { + return new Config(allowRuntimeSpecs, allowEmptyACLSpecs); + } + + @Bean + RuntimeSecretProcessingService runtimeSecretProcessingService( + DefinitionBrowserRegistry definitionBrowserRegistry, + SpecLifecycleService specLifecycleService, + GrantService grantService, + EnvAwareSpecRegistry specRegistry + ) { + return new RuntimeSecretProcessingService( + definitionBrowserRegistry, + new ContextRegistry(), + grantService, + specLifecycleService, + specRegistry + ); + } + + @Bean + DefinitionBrowserRegistry definitionBrowserRegistry(List browsers) { + return new DefinitionBrowserRegistry(browsers); + } + + @Bean + SpecLifecycleService secretSpecService(Cache cache, ResolverService resolverService, Config config) { + return new DefaultSpecLifecycleService(new EnvAwareSpecRegistry(), cache, resolverService, config); + } + + @Bean + Cache secretCache() { + return new SimpleOffHeapCache(); + } + + @Bean + GrantService grantService(Config config) { + return new DefaultGrantService(new GrantRegistry(), config); + } + + @Bean + EnvAwareSpecRegistry envAwareSpecRegistry() { + return new EnvAwareSpecRegistry(); + } + + @Bean + @Conditional(EnvironmentCondition.class) + SecretProviderDeployer runtimeSecretProviderDeployer(Environment environment) { + return new FromConfigurationSecretProviderDeployer(environment); + } + + @Bean + ResolverService runtimeSecretResolver() { + SecretProviderRegistry secretProviderRegistry = new SecretProviderRegistry(); + return new DefaultRuntimeResolver(secretProviderRegistry); + } + + @Bean + ContextUpdater elContextUpdater( + Cache cache, + GrantService grantService, + SpecLifecycleService specLifecycleService, + EnvAwareSpecRegistry specRegistry + ) { + return new ContextUpdater(cache, grantService, specLifecycleService, specRegistry); + } + + static class EnvironmentCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata ignore) { + return context.getEnvironment().getProperty("api.secrets.allowProvidersFromConfiguration", Boolean.class, true); + } + } +} 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..970c85700 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCache.java @@ -0,0 +1,92 @@ +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.Entry; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +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 CacheKey put(String envId, String naturalId, Entry value) { + final CacheKey cacheKey = new CacheKey(envId, naturalId); + var bytes = serialize(value); + final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length); + data.put(cacheKey, byteBuffer); + byteBuffer.put(bytes); + return cacheKey; + } + + @Override + public Optional get(String envId, String naturalId) { + ByteBuffer byteBuffer = data.get(new CacheKey(envId, naturalId)); + 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(String envId, String naturalId, Supplier supplier) { + data.computeIfAbsent( + new CacheKey(envId, naturalId), + key -> { + Entry value = supplier.get(); + byte[] stringAsBytes = serialize(value); + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(stringAsBytes.length); + byteBuffer.put(stringAsBytes); + return byteBuffer; + } + ); + } + + @Override + public void evict(String envId, String naturalId) { + data.remove(new CacheKey(envId, naturalId)); + } + + 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/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java new file mode 100644 index 000000000..ee67ec284 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java @@ -0,0 +1,128 @@ +package com.graviteesource.services.runtimesecrets.api.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.graviteesource.services.runtimesecrets.discovery.RefParser; +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 RefTest { + + 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, expression.length())) + .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/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..bed5a9e37 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefDiscovererTest.java @@ -0,0 +1,88 @@ +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/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..a996f4a40 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantServiceTest.java @@ -0,0 +1,134 @@ +package com.graviteesource.services.runtimesecrets.grant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.graviteesource.services.runtimesecrets.config.Config; +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.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(true, true); + this.cut = new DefaultGrantService(new GrantRegistry(), config); + } + + public static Stream grants() { + return Stream.of( + arguments("null spec", context("dev", "api", "123"), null, true, "no spec found"), + arguments("no acl same env", context("dev", "api", "123"), spec("dev", null), true, null), + arguments("empty acl same env", context("dev", "api", "123"), spec("dev", new ACLs(List.of(), List.of())), true, null), + arguments("no acl diff env", context("dev", "api", "123"), spec("test", null), false, null), + arguments( + "def acl ok", + context("dev", "api", "123"), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null)), + true, + null + ), + arguments( + "def acl wrong id", + context("dev", "api", "123"), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("456"))), null)), + false, + null + ), + arguments( + "def acl wrong kind", + context("dev", "api", "123"), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("dict", List.of("123"))), null)), + false, + null + ), + arguments( + "def acl many", + context("dev", "api", "123"), + spec( + "dev", + new ACLs( + List.of(new ACLs.DefinitionACL("dict", List.of("123")), new ACLs.DefinitionACL("api", List.of("123", "456"))), + null + ) + ), + true, + null + ), + arguments( + "plugin acl ok", + context("dev", "api", "123", plugin("foo")), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null)))), + true, + null + ), + arguments( + "plugin acl ko", + context("dev", "api", "123", plugin("foo")), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("bar", null)))), + false, + null + ), + arguments( + "plugin acl ok many", + context("dev", "api", "123", plugin("foo")), + spec( + "dev", + new ACLs( + List.of(new ACLs.DefinitionACL("api", List.of("123"))), + List.of(new ACLs.PluginACL("bar", null), new ACLs.PluginACL("foo", null)) + ) + ), + true, + null + ) + ); + } + + @MethodSource("grants") + @ParameterizedTest(name = "{0}") + void should_authorize(String name, DiscoveryContext context, Spec spec, boolean granted, String error) { + if (error != null) { + assertThatCode(() -> cut.authorize(context, spec)).hasMessageContaining(error); + } else { + assertThat(cut.authorize(context, spec)).isEqualTo(granted); + } + } + + static DiscoveryContext context(String env, String kind, String id, PayloadLocation... payloads) { + return new DiscoveryContext( + null, + env, + new Ref(Ref.MainType.NAME, new Ref.Expression("secret", false), null, null, "<< secret >>"), + new DiscoveryLocation(new DiscoveryLocation.Definition(kind, id), payloads) + ); + } + + static Spec spec(String env, ACLs acls) { + return new Spec(null, "secret", null, null, null, false, 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/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..5b5deeba8 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/storage/SimpleOffHeapCacheTest.java @@ -0,0 +1,104 @@ +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.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("dev", "secret_error", new Entry(Entry.Type.ERROR, null, "500")); + cut.put("test", "secret_empty", new Entry(Entry.Type.EMPTY, null, "204")); + cut.put("prod", "secret_not_found", new Entry(Entry.Type.NOT_FOUND, null, "404")); + + assertThat(cut.get("dev", "secret_error")).get().extracting("type", "error").containsExactly(Entry.Type.ERROR, "500"); + assertThat(cut.get("test", "secret_empty")).get().extracting("type", "error").containsExactly(Entry.Type.EMPTY, "204"); + assertThat(cut.get("prod", "secret_not_found")) + .isPresent() + .get() + .extracting("type", "error") + .containsExactly(Entry.Type.NOT_FOUND, "404"); + } + + @Test + void should_store_segmented_data() { + cut.put("dev", "secret", new Entry(Entry.Type.VALUE, Map.of("foo", new Secret("bar")), null)); + cut.put("test", "secret", new Entry(Entry.Type.VALUE, Map.of("buz", new Secret("puk")), null)); + assertThat(cut.get("dev", "secret")) + .get() + .usingRecursiveAssertion() + .isEqualTo(new Entry(Entry.Type.VALUE, Map.of("foo", new Secret("bar")), null)); + assertThat(cut.get("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( + "dev", + "secret", + new Entry(Entry.Type.VALUE, Map.of("redis-password", new Secret("123456"), "ldap-password", new Secret("azerty")), null) + ); + assertThat(cut.get("dev", "secret")) + .get() + .extracting(entry -> entry.value().values().stream().map(Secret::asString).toList()) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactlyInAnyOrder("123456", "azerty"); + assertThat(cut.get("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("dev", "secret", dbPasswords); + dbPasswordsAssert(cut.get("dev", "secret")); + + // no override as does not exists + cut.computeIfAbsent("dev", "secret", () -> new Entry(Entry.Type.VALUE, Map.of(), null)); + dbPasswordsAssert(cut.get("dev", "secret")); + + // eviction + cut.evict("dev", "secret"); + assertThat(cut.get("dev", "secret")).isNotPresent(); + + cut.computeIfAbsent("dev", "secret", () -> dbPasswords); + dbPasswordsAssert(cut.get("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-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/pom.xml b/gravitee-node-secrets/pom.xml index 1094b833d..954bf70f2 100644 --- a/gravitee-node-secrets/pom.xml +++ b/gravitee-node-secrets/pom.xml @@ -34,6 +34,7 @@ gravitee-node-secrets-plugin-handler gravitee-node-secrets-service + gravitee-node-secrets-runtime @@ -51,4 +52,12 @@ + + + com.esotericsoftware + kryo + 5.6.0 + + + From 4a7d67d51997b383483fb124e618a6fbee224c69 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Tue, 17 Sep 2024 16:24:24 +0200 Subject: [PATCH 02/15] feat: add mock secret provider plugin --- .../gravitee-secret-provider-mock/pom.xml | 136 ++++++++++++++++++ .../src/main/assembly/plugin-assembly.xml | 48 +++++++ .../plugin/mock/MockSecretLocation.java | 23 +++ .../plugin/mock/MockSecretProvider.java | 44 ++++++ .../mock/MockSecretProviderConfiguration.java | 31 ++++ .../mock/MockSecretProviderFactory.java | 16 +++ .../src/main/resources/plugin.properties | 7 + .../plugin/mock/MockSecretProviderTest.java | 52 +++++++ gravitee-node-secrets/pom.xml | 1 + 9 files changed, 358 insertions(+) create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/assembly/plugin-assembly.xml create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretLocation.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProvider.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderFactory.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/resources/plugin.properties create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/test/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderTest.java 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..1862aee66 --- /dev/null +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml @@ -0,0 +1,136 @@ + + + + 4.0.0 + + io.gravitee.node + gravitee-node-secrets + 6.4.2 + + + gravitee-secret-provider-mock + Gravitee.io - Node - Secrets - Mock Provider + + + 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 + + + + + + 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..d5cebfe08 --- /dev/null +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProvider.java @@ -0,0 +1,44 @@ +package io.gravitee.node.secrets.plugin.mock; + +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.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +@RequiredArgsConstructor +public class MockSecretProvider implements SecretProvider { + + public static final String PLUGIN_ID = "mock"; + + private final MockSecretProviderConfiguration configuration; + + @Override + public Maybe resolve(SecretMount secretMount) { + MockSecretLocation location = MockSecretLocation.fromLocation(secretMount.location()); + Map secretMap = ConfigHelper.removePrefix(configuration.getSecrets(), location.secret()); + if (secretMap.isEmpty()) { + return Maybe.empty(); + } + return Maybe.just(SecretMap.of(secretMap)); + } + + @Override + public Flowable watch(SecretMount secretMount) { + return Flowable.empty(); + } + + @Override + public SecretMount fromURL(SecretURL url) { + return new SecretMount(PLUGIN_ID, MockSecretLocation.fromUrl(url), url.key(), url); + } +} diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java new file mode 100644 index 000000000..d70ae4109 --- /dev/null +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java @@ -0,0 +1,31 @@ +package io.gravitee.node.secrets.plugin.mock; + +import io.gravitee.node.api.secrets.SecretManagerConfiguration; +import io.gravitee.node.api.secrets.util.ConfigHelper; +import java.util.Map; +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; + + @Getter + private final Map secrets; + + public MockSecretProviderConfiguration(Map config) { + this.enabled = ConfigHelper.getProperty(config, Fields.enabled, Boolean.class, false); + this.secrets = ConfigHelper.removePrefix(config, "secrets"); + } + + @Override + public boolean isEnabled() { + return enabled; + } +} 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..3db78ed87 --- /dev/null +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderFactory.java @@ -0,0 +1,16 @@ +package io.gravitee.node.secrets.plugin.mock; + +import io.gravitee.node.api.secrets.SecretProvider; +import io.gravitee.node.api.secrets.SecretProviderFactory; + +/** + * @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/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..57d465ea0 --- /dev/null +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/src/test/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderTest.java @@ -0,0 +1,52 @@ +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.SecretMap; +import io.gravitee.node.api.secrets.model.SecretMount; +import io.gravitee.node.api.secrets.model.SecretURL; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +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 MockSecretProviderTest { + + private SecretProvider cut; + + @BeforeEach + void setup() { + Map conf = Map.of("enabled", true, "secrets.redis.password", "r3d1s", "secrets.ldap.password", "1da9"); + 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().assertValue(SecretMap.of(Map.of("password", "r3d1s"))); + + SecretMount secretMountLdap = cut.fromURL(SecretURL.from("secret://mock/ldap")); + cut.resolve(secretMountLdap).test().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().assertNoErrors().assertComplete(); + } +} diff --git a/gravitee-node-secrets/pom.xml b/gravitee-node-secrets/pom.xml index 954bf70f2..2fca06b06 100644 --- a/gravitee-node-secrets/pom.xml +++ b/gravitee-node-secrets/pom.xml @@ -35,6 +35,7 @@ gravitee-node-secrets-plugin-handler gravitee-node-secrets-service gravitee-node-secrets-runtime + gravitee-secret-provider-mock From 900f908c616256b1c0455ce01227e1ecb0c54721 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Thu, 3 Oct 2024 15:11:43 +0200 Subject: [PATCH 03/15] feat: prep for poc demo --- .../node/api/secrets/model/SecretMount.java | 2 +- .../node/api/secrets/model/SecretURL.java | 8 +- .../runtime/discovery/ContextRegistry.java | 14 + .../runtime/discovery/DefinitionBrowser.java | 4 +- .../discovery/DefinitionPayloadNotifier.java | 4 +- .../api/secrets/runtime/discovery/Ref.java | 15 +- .../node/api/secrets/runtime/grant/Grant.java | 7 + .../secrets/runtime/grant/GrantService.java | 7 +- .../runtime/providers/ResolverService.java | 2 +- .../providers/SecretProviderDeployer.java | 4 +- .../node/api/secrets/runtime/spec/Spec.java | 6 +- gravitee-node-container/pom.xml | 6 + .../gravitee/node/container/AbstractNode.java | 2 + .../spring/SpringBasedContainer.java | 2 + .../gravitee-node-secrets-runtime/pom.xml | 36 +- ...a => RuntimeSecretsProcessingService.java} | 50 +- .../runtimesecrets/RuntimeSecretsService.java | 39 ++ .../runtimesecrets/config/Config.java | 24 +- .../discovery/ContextRegistry.java | 53 -- .../discovery/DefaultContextRegistry.java | 92 ++++ .../discovery/DefinitionBrowserRegistry.java | 15 + .../discovery/PayloadRefParser.java | 19 + .../runtimesecrets/discovery/RefParser.java | 23 +- .../runtimesecrets/el/ContextUpdater.java | 19 +- .../services/runtimesecrets/el/Formatter.java | 52 +- .../services/runtimesecrets/el/Result.java | 15 + .../services/runtimesecrets/el/Service.java | 51 +- .../SecretSpelSecuredEvaluationContext.java | 39 ++ .../SecretSpelSecuredMethodResolver.java | 42 ++ .../el/engine/SecretSpelTemplateContext.java | 29 ++ .../el/engine/SecretSpelTemplateEngine.java | 30 ++ .../errors/SecretAccessDeniedException.java | 15 + .../errors/SecretEmptyException.java | 15 + .../errors/SecretKeyNotFoundException.java | 15 + .../errors/SecretNotFoundException.java | 15 + .../errors/SecretProviderException.java | 15 + .../SecretProviderNotFoundException.java | 15 + .../errors/SecretRefParsingException.java | 15 + .../errors/SecretSpecNotFoundException.java | 15 + .../grant/DefaultGrantService.java | 66 ++- .../runtimesecrets/grant/GrantRegistry.java | 26 +- .../providers/DefaultResolverService.java | 50 ++ .../providers/DefaultRuntimeResolver.java | 35 -- ...omConfigurationSecretProviderDeployer.java | 35 -- .../providers/SecretProviderRegistry.java | 42 +- ...omConfigurationSecretProviderDeployer.java | 122 +++++ .../spec/DefaultSpecLifecycleService.java | 155 ++++-- .../runtimesecrets/spec/SpecRegistry.java | 159 ++++++ .../spec/registry/EnvAwareSpecRegistry.java | 51 -- .../spec/registry/SpecRegistry.java | 91 ---- .../runtimesecrets/spring/BeanFactory.java | 116 ----- .../spring/RuntimeSecretsBeanFactory.java | 182 +++++++ .../spring/SecretTemplateEngineFactory.java | 33 ++ .../storage/SimpleOffHeapCache.java | 15 + .../main/resources/META-INF/spring.factories | 1 + .../RuntimeSecretsProcessingServiceTest.java | 493 ++++++++++++++++++ .../discovery/RefDiscovererTest.java | 15 + .../RefParserTest.java} | 20 +- .../runtimesecrets/el/ServiceTest.java | 173 ++++++ .../grant/DefaultGrantServiceTest.java | 182 +++++-- ...nfigurationSecretProviderDeployerTest.java | 106 ++++ .../spec/DefaultSpecLifecycleServiceTest.java | 135 +++++ .../storage/SimpleOffHeapCacheTest.java | 15 + .../testsupport/PluginManagerHelper.java | 137 +++++ .../testsupport/SpecFixtures.java | 70 +++ .../src/test/resources/v4-api.json | 269 ++++++++++ .../AbstractSecretProviderDispatcher.java | 9 +- .../gravitee-secret-provider-mock/pom.xml | 11 + .../plugin/mock/MockSecretProvider.java | 66 ++- .../mock/MockSecretProviderConfiguration.java | 31 -- .../mock/MockSecretProviderException.java | 12 + .../mock/MockSecretProviderFactory.java | 1 + .../plugin/mock/conf/ConfiguredError.java | 3 + .../plugin/mock/conf/ConfiguredEvent.java | 6 + .../conf/MockSecretProviderConfiguration.java | 83 +++ .../plugin/mock/MockSecretProviderTest.java | 116 ++++- 76 files changed, 3367 insertions(+), 596 deletions(-) create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/ContextRegistry.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/Grant.java rename gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/{RuntimeSecretProcessingService.java => RuntimeSecretsProcessingService.java} (72%) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsService.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredEvaluationContext.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelSecuredMethodResolver.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateContext.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretSpelTemplateEngine.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultResolverService.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/config/FromConfigurationSecretProviderDeployer.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecRegistry.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java delete mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/RuntimeSecretsBeanFactory.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/SecretTemplateEngineFactory.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/spring.factories create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java rename gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/{api/model/RefTest.java => discovery/RefParserTest.java} (88%) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/el/ServiceTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployerTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleServiceTest.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/PluginManagerHelper.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/SpecFixtures.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/v4-api.json delete mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderException.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredError.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/ConfiguredEvent.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/MockSecretProviderConfiguration.java 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 d1d2fd2c4..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, boolean withRetries) { +public record SecretMount(String provider, SecretLocation location, String key, SecretURL secretURL, boolean retryOnError) { /** * Test the presence of a key * 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 4173d7c7e..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 @@ -17,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('&'); @@ -25,6 +25,10 @@ public record SecretURL(String provider, String path, String key, Multimap @@ -93,7 +97,7 @@ public static SecretURL from(String url, boolean includesSchema) { 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/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..6df3d92d1 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/ContextRegistry.java @@ -0,0 +1,14 @@ +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); +} 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 index 1a7e374ef..592916375 100644 --- 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 @@ -7,9 +7,9 @@ * @author GraviteeSource Team */ public interface DefinitionBrowser { - boolean canHandle(T definition); + boolean canHandle(Object definition); Definition getDefinitionKindLocation(T definition, Map metadata); - void findPayloads(DefinitionPayloadNotifier notifier); + 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 index 3a4ac4646..ddf3f3ea1 100644 --- 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 @@ -1,9 +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); + void onPayload(String payload, PayloadLocation location, Consumer updatedPayload); } 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 index 064913f48..dbfd9141e 100644 --- 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 @@ -33,8 +33,19 @@ public enum SecondaryType { public static final String URI_KEY_SEPARATOR = ":"; - public Spec toRuntimeSpec(String envId) { - return new Spec(null, null, mainExpression.value(), secondaryExpression().value(), null, false, true, null, null, envId); + public Spec asOnTheFlySpec(String envId) { + return new Spec( + null, + null, + mainExpression().value(), + secondaryExpression().value(), + null, + mainType() == MainType.URI && mainExpression.isLiteral() && secondaryType() == null, + true, + null, + null, + envId + ); } /** 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..9d02b8cc8 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/Grant.java @@ -0,0 +1,7 @@ +package io.gravitee.node.api.secrets.runtime.grant; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public record Grant(String naturalId, String key) {} 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 index 6dcba6514..2c6da1d9f 100644 --- 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 @@ -2,17 +2,18 @@ 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 { - boolean isGranted(String token); + Optional getGrant(String contextId); - boolean authorize(DiscoveryContext context, Spec spec); + boolean isGranted(DiscoveryContext context, Spec spec); - void grant(DiscoveryContext context); + void 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 index 56305d41c..888a08950 100644 --- 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 @@ -12,5 +12,5 @@ public interface ResolverService { Single resolve(String envId, SecretMount secretMount); - SecretMount toSecretMount(String envId, SecretURL secretURL); + 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 index 8da82de32..136f59c3a 100644 --- 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 @@ -7,5 +7,7 @@ * @author GraviteeSource Team */ public interface SecretProviderDeployer { - void deploy(String id, String pluginId, Map config, String envId); + 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/Spec.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java index 5f84bf879..59956422e 100644 --- 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 @@ -1,6 +1,6 @@ package io.gravitee.node.api.secrets.runtime.spec; -import static io.gravitee.node.api.secrets.runtime.discovery.Ref.URI_KEY_SEPARATOR; +import static io.gravitee.node.api.secrets.runtime.discovery.Ref.formatUriAndKey; import io.gravitee.node.api.secrets.model.SecretURL; import java.util.List; @@ -18,7 +18,7 @@ public record Spec( String key, List children, boolean usesDynamicKey, - boolean isRuntime, + boolean isOnTheFly, RenewalPolicy renewalPolicy, ACLs acls, String envId @@ -42,7 +42,7 @@ public Optional findChildrenFromUri(String query) { } public String uriAndKey() { - return uri + URI_KEY_SEPARATOR + key; + return formatUriAndKey(uri, key); } public SecretURL toSecretURL() { diff --git a/gravitee-node-container/pom.xml b/gravitee-node-container/pom.xml index 5b218238f..c1635d0b9 100644 --- a/gravitee-node-container/pom.xml +++ b/gravitee-node-container/pom.xml @@ -169,5 +169,11 @@ mockito-core test + + io.gravitee.node + gravitee-node-secrets-runtime + 6.4.2 + compile + 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-secrets/gravitee-node-secrets-runtime/pom.xml b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml index c411edeef..a7126e39a 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml @@ -45,7 +45,7 @@ io.gravitee.el gravitee-expression-language - 3.1.0 + 3.2.3 compile @@ -54,6 +54,38 @@ awaitility test - + + io.gravitee.node + gravitee-secret-provider-mock + 6.4.2 + + + org.springframework + spring-test + test + + + org.yaml + snakeyaml + 2.2 + test + + + org.springframework.security + spring-security-core + test + + + io.gravitee.apim.definition + gravitee-apim-definition-model + 4.5.0-SNAPSHOT + test + + + io.gravitee.apim.definition + gravitee-apim-definition-jackson + 4.5.0-SNAPSHOT + test + diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java similarity index 72% rename from gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java rename to gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java index bc86cce51..1aa01ad1b 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretProcessingService.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java @@ -1,16 +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; -import com.graviteesource.services.runtimesecrets.discovery.ContextRegistry; 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.registry.EnvAwareSpecRegistry; +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; @@ -23,25 +38,25 @@ */ @Slf4j @RequiredArgsConstructor -public class RuntimeSecretProcessingService { +public class RuntimeSecretsProcessingService { private final DefinitionBrowserRegistry definitionBrowserRegistry; private final ContextRegistry contextRegistry; + private final SpecRegistry specRegistry; private final GrantService grantService; private final SpecLifecycleService specLifecycleService; - private final EnvAwareSpecRegistry specRegistry; /** *
  • finds a {@link DefinitionBrowser}
  • *
  • Run it to get {@link DiscoveryContext}
  • *
  • Inject EL {@link PayloadRefParser}
  • - *
  • Find {@link Spec}
  • + *
  • Find {@link Spec} or create on the fly
  • *
  • Grant {@link DiscoveryContext}
  • * @param definition the secret naturalId container * @param metadata some optional metadata * @param the kind of subject */ - void processSecrets(String envId, @Nonnull T definition, @Nullable Map metadata) { + public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) { Optional> browser = definitionBrowserRegistry.findBrowser(definition); if (browser.isEmpty()) { log.info("No definition browser found for kind [{}]", definition.getClass()); @@ -50,8 +65,11 @@ void processSecrets(String envId, @Nonnull T definition, @Nullable Map definitionBrowser = browser.get(); Definition rootDefinition = definitionBrowser.getDefinitionKindLocation(definition, metadata); - DefaultPayloadNotifier notifier = new DefaultPayloadNotifier(rootDefinition, envId); - definitionBrowser.findPayloads(notifier); + + log.info("Finding secret in definition: {}", rootDefinition); + + DefaultPayloadNotifier notifier = new DefaultPayloadNotifier(rootDefinition, envId, specRegistry); + definitionBrowser.findPayloads(definition, notifier); // register contexts by naturalId and definition for (DiscoveryContext context : notifier.getContextList()) { @@ -61,9 +79,12 @@ void processSecrets(String envId, @Nonnull T definition, @Nullable Map updatedPayload) { PayloadRefParser payloadRefParser = new PayloadRefParser(payload); List discoveryContexts = payloadRefParser .runDiscovery() @@ -112,6 +135,7 @@ public void onPayload(String payload, PayloadLocation payloadLocation) { }) .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..5f831ab04 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsService.java @@ -0,0 +1,39 @@ +package com.graviteesource.services.runtimesecrets; + +import io.gravitee.common.service.AbstractService; +import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer; +import io.gravitee.node.api.secrets.runtime.spec.Spec; +import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +@RequiredArgsConstructor +public class RuntimeSecretsService extends AbstractService { + + private final RuntimeSecretsProcessingService runtimeSecretsProcessingService; + private final SpecLifecycleService specLifecycleService; + private final SecretProviderDeployer secretProviderDeployer; + + @Override + protected void doStart() throws Exception { + secretProviderDeployer.init(); + } + + 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) { + runtimeSecretsProcessingService.onDefinitionDeploy(envId, definition, metadata); + } +} 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 index c0a87ac24..b7622c648 100644 --- 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 @@ -1,10 +1,28 @@ +/* + * 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(boolean allowOnTheFlySpecs, boolean allowEmptyACLSpecs) { - public static final String ALLOW_EMPTY_ACL_SPECS = "api.secrets.allowEmptyNoACLsSpecs"; - public static final String ALLOW_ON_THE_FLY_SPECS = "api.secrets.allowOnTheFlySpecs"; +public record Config(boolean onTheFlySpecsEnabled, long onTheFlySpecsDelayBeforeRetryMs, boolean allowEmptyACLSpecs) { + public static final String CONFIG_PREFIX = "api.secrets"; + public static final String ALLOW_EMPTY_NO_ACL_SPECS = CONFIG_PREFIX + ".allowNoACLsSpecs"; + public static final String ON_THE_FLY_SPECS_ENABLED = CONFIG_PREFIX + ".onTheFlySpecs.enabled"; + public static final String ON_THE_FLY_SPECS_DELAY_BEFORE_RETRY_MS = CONFIG_PREFIX + ".onTheFlySpecs.delayBeforeRetryMs"; + 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/discovery/ContextRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java deleted file mode 100644 index 8ec071864..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/ContextRegistry.java +++ /dev/null @@ -1,53 +0,0 @@ -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.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.List; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public class 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()) { - byName.put(context.ref().mainExpression().value(), context); - } - if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { - byUri.put(context.ref().mainExpression().value(), context); - if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { - byUriAndKey.put(context.ref().uriAndKey(), context); - } - } - byDefinitionSpec.put(definition, context); - } - - public List findBySpec(Spec spec) { - List result = new ArrayList<>(); - if (spec.name() != null && !spec.name().isEmpty()) { - result.addAll(byName.get(spec.name())); - } - if (spec.uri() != null && !spec.uri().isEmpty()) { - result.addAll(byUri.get(spec.name())); - } - if (spec.key() != null && !spec.key().isEmpty()) { - result.addAll(byUriAndKey.get(spec.uriAndKey())); - } - return result; - } - - public List getByDefinition(Definition definition) { - return (List) byDefinitionSpec.get(definition); - } -} 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..9e620df12 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java @@ -0,0 +1,92 @@ +/* + * 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); + } + + 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()) { + byName.put(context.ref().mainExpression().value(), context); + } + if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { + byUri.put(context.ref().mainExpression().value(), context); + if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { + byUriAndKey.put(context.ref().uriAndKey(), context); + } + } + byDefinitionSpec.put(definition, context); + } + + public List findBySpec(Spec spec) { + List result = new ArrayList<>(); + if (spec.name() != null && !spec.name().isEmpty()) { + result.addAll(byName.get(spec.name())); + } + if (spec.uri() != null && !spec.uri().isEmpty()) { + result.addAll(byUri.get(spec.name())); + } + if (spec.key() != null && !spec.key().isEmpty()) { + result.addAll(byUriAndKey.get(spec.uriAndKey())); + } + return result; + } + + public List getByDefinition(String envId, Definition definition) { + return (List) byDefinitionSpec.get(definition); + } + } +} 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 index d66827147..c7073977b 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index b8c6c6de3..985f1d4f4 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; @@ -77,4 +92,8 @@ public String replaceRefs(List expressions) { 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 index cc945c56d..abf53a089 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; @@ -57,7 +72,7 @@ public static Ref parse(String ref) { } catch (IllegalArgumentException e) { throw enumError("unknown kind: '%s' for secret reference '%s'".formatted(typeString, ref)); } - final RefParsing refParsing = mainExpression(buffer); + final RefParsing refParsing = mainExpression(buffer, ref); Ref.SecondaryType secondaryType = null; if (refParsing.secondaryType() != null) { @@ -107,7 +122,7 @@ private static String mainType(StringBuilder buffer) { } if (isEL(buffer.toString())) { throw new SecretRefParsingException( - "EL expression must be preceded by '%s' or '%s' when starting the secret reference".formatted( + "EL expression must be preceded by '%s' or '%s' when located at the beginning of secret reference".formatted( NAME_TYPE.stripLeading(), URI_TYPE.stripLeading() ) @@ -122,7 +137,7 @@ private static String mainType(StringBuilder buffer) { record RefParsing(String mainExpression, String secondaryType, String secondaryExpression) {} - private static RefParsing mainExpression(StringBuilder buffer) { + private static RefParsing mainExpression(StringBuilder buffer, String ref) { String foundToken = null; String expression = null; int end = 0; @@ -137,7 +152,7 @@ private static RefParsing mainExpression(StringBuilder buffer) { } if (foundToken == null) { - throw new SecretRefParsingException("reference %s syntax is incorrect looks like nothing is specified"); + throw new SecretRefParsingException("reference %s syntax is incorrect".formatted(ref)); } String uriOrName = expression.trim(); diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java index cee1d535d..c74b612b0 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java @@ -1,6 +1,21 @@ +/* + * 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 com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; +import com.graviteesource.services.runtimesecrets.spec.SpecRegistry; import io.gravitee.el.TemplateContext; import io.gravitee.node.api.secrets.runtime.grant.GrantService; import io.gravitee.node.api.secrets.runtime.spec.SpecLifecycleService; @@ -17,7 +32,7 @@ public class ContextUpdater { private final Cache cache; private final GrantService grantService; private final SpecLifecycleService specLifecycleService; - private final EnvAwareSpecRegistry specRegistry; + private final SpecRegistry specRegistry; public void addRuntimeSecretsService(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/el/Formatter.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java index dea549c3b..a5473f64b 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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 io.gravitee.node.api.secrets.runtime.discovery.Definition; @@ -12,11 +27,12 @@ */ public class Formatter { - public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s', '%s', '%s', %s)}"; + public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s', '%s')}"; + public static final String FROM_GRANT_EL_KEY_TEMPLATE = "{#secrets.fromGrant('%s', '%s', %s)}"; 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 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()) { @@ -24,13 +40,25 @@ public static String computeELFromStatic(DiscoveryContext context, String envId) } final String mainSpec = context.ref().mainExpression().value(); String el; - switch (context.ref().secondaryType()) { - case KEY -> el = fromGrant(context.id(), envId, mainSpec, context.ref().secondaryExpression()); - 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())); + if (context.ref().secondaryType() != null) { + switch (context.ref().secondaryType()) { + case KEY -> { + if (context.ref().secondaryExpression().isLiteral()) { + el = fromGrant(context.id(), envId); + } else { + el = fromGrant(context.id(), envId, 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(), envId); } return el; } @@ -80,8 +108,12 @@ private static String fromGrantWithTemplate( return FROM_GRANT_WITH_TEMPLATE.formatted(methodSuffix, id, envId, literalExpression, quoteLiteral(secondaryExpression)); } - private static String fromGrant(UUID id, String envId, String expression, Ref.Expression keySpec) { - return FROM_GRANT_TEMPLATE.formatted(id, envId, expression, quoteLiteral(keySpec)); + private static String fromGrant(UUID id, String envId) { + return FROM_GRANT_TEMPLATE.formatted(id, envId); + } + + private static String fromGrant(UUID id, String envId, String key) { + return FROM_GRANT_EL_KEY_TEMPLATE.formatted(id, envId, key); } private static String quoteLiteral(Ref.Expression expression) { 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 index be7c59635..89c1f0512 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; /** 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 index 161427246..93d330afe 100644 --- 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 @@ -1,20 +1,37 @@ +/* + * 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.registry.EnvAwareSpecRegistry; +import com.graviteesource.services.runtimesecrets.spec.SpecRegistry; 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.Entry; import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; /** @@ -27,19 +44,35 @@ public class Service { private final Cache cache; private final GrantService grantService; private final SpecLifecycleService specLifecycleService; - private final EnvAwareSpecRegistry specRegistry; + private final SpecRegistry specRegistry; - public String fromGrant(String token, String envId, String expression, String key) { - boolean granted = grantService.isGranted(token); - if (!granted) { - return resultToValue(new Result(Result.Type.DENIED, "secret [%s] is denied in environment [%s]".formatted(expression, envId))); + public String fromGrant(String contextId, String envId) { + Optional grantOptional = grantService.getGrant(contextId); + if (grantOptional.isEmpty()) { + return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); } + return getFromCache(envId, grantOptional.get(), grantOptional.get().key()); + } + + public String fromGrant(String contextId, String envId, String key) { + Optional grantOptional = grantService.getGrant(contextId); + if (grantOptional.isEmpty()) { + return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); + } + return getFromCache(envId, grantOptional.get(), key); + } + + private String getFromCache(String envId, Grant grant, String key) { return resultToValue( toResult( cache - .get(envId, expression) + .get(envId, grant.naturalId()) .orElse( - new Entry(Entry.Type.EMPTY, null, "no value in cache for [%s] in environment [%s]".formatted(expression, envId)) + new Entry( + Entry.Type.EMPTY, + null, + "no value in cache for [%s] in environment [%s]".formatted(grant.naturalId(), envId) + ) ), key ) @@ -75,7 +108,7 @@ public String fromELWithName(String envId, String name, String definitionKind, S } private String grantAndGet(String envId, String definitionKind, String definitionId, Spec spec, Ref ref, String naturalId, String key) { - boolean granted = grantService.authorize( + boolean granted = grantService.isGranted( new DiscoveryContext(null, envId, ref, new DiscoveryLocation(new DiscoveryLocation.Definition(definitionKind, definitionId))), spec ); 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/errors/SecretAccessDeniedException.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/errors/SecretAccessDeniedException.java index 1141a5dad..9521f2f68 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index a33bb6d63..25a275606 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index f2fc6f4e8..b7316ff95 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index 27cd15442..b85486dc9 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index ad9af9a76..dd9b5ade4 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index 2f060cd7a..db0d99d6c 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index 12d0aacde..d4de949c2 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index 7370d3886..ad51b4fb5 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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 index 84c8833c0..96c41703a 100644 --- 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 @@ -1,18 +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.grant; -import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_ACL_SPECS; -import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_ON_THE_FLY_SPECS; +import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_NO_ACL_SPECS; +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 com.graviteesource.services.runtimesecrets.errors.SecretSpecNotFoundException; 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 java.util.Arrays; import java.util.Objects; +import java.util.Optional; import java.util.function.Predicate; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; @@ -30,14 +48,14 @@ public class DefaultGrantService implements GrantService { private final Config config; @Override - public boolean authorize(@Nonnull DiscoveryContext context, Spec spec) { + public boolean isGranted(@Nonnull DiscoveryContext context, Spec spec) { if (spec == null) { throw new SecretSpecNotFoundException( "no spec found or created on-the-fly for ref [%s] in envId [%s], %s=%s".formatted( context.ref().rawRef(), context.envId(), - ALLOW_ON_THE_FLY_SPECS, - config.allowOnTheFlySpecs() + ON_THE_FLY_SPECS_ENABLED, + config.onTheFlySpecsEnabled() ) ); } @@ -46,24 +64,25 @@ public boolean authorize(@Nonnull DiscoveryContext context, Spec spec) { log.warn( "secret spec for ref [{}] is not granted because is does not contains ACLs and this is not allowed. see: {}", context.ref().rawRef(), - ALLOW_EMPTY_ACL_SPECS + ALLOW_EMPTY_NO_ACL_SPECS ); return false; } else { - return Objects.equals(context.envId(), spec.envId()); + return checkSpec(context).test(spec); } } - return checkACLs(spec, context); + return checkSpec(context).test(spec) && checkACLs(context).test(spec.acls()); } - public boolean isGranted(@Nonnull String token) { - return grantRegistry.exists(token); + @Override + public void grant(@Nonnull DiscoveryContext context, Spec spec) { + grantRegistry.register(context.id().toString(), new Grant(spec.naturalId(), spec.key())); } @Override - public void grant(@Nonnull DiscoveryContext context) { - grantRegistry.register(context); + public Optional getGrant(String contextId) { + return Optional.ofNullable(grantRegistry.get(contextId)); } @Override @@ -71,13 +90,26 @@ public void revoke(@Nonnull DiscoveryContext context) { grantRegistry.unregister(context); } - private boolean checkACLs(Spec spec, DiscoveryContext 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 defKind = acls -> + Predicate defKindMatch = acls -> acls.definitions().stream().anyMatch(defACLs -> defACLs.kind().contains(context.location().definition().kind())); Predicate noDefId = acls -> @@ -85,12 +117,12 @@ private boolean checkACLs(Spec spec, DiscoveryContext context) { acls.definitions().isEmpty() || acls.definitions().stream().allMatch(def -> def.ids() == null || def.ids().isEmpty()); - Predicate defId = acls -> + 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 plugin = acls -> + Predicate pluginMatch = acls -> acls .plugins() .stream() @@ -106,6 +138,6 @@ private boolean checkACLs(Spec spec, DiscoveryContext context) { ) ); - return noDefKind.or(defKind).and(noDefId.or(defId)).and(noPlugin.or(plugin)).test(spec.acls()); + 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 index fcab420a7..b16018241 100644 --- 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 @@ -1,6 +1,22 @@ +/* + * 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; @@ -12,10 +28,10 @@ */ public class GrantRegistry { - private final Map grants = new ConcurrentHashMap<>(); + private final Map grants = new ConcurrentHashMap<>(); - public void register(DiscoveryContext context) { - grants.put(context.id().toString(), null); + public void register(String id, Grant grant) { + grants.put(id, grant); } public void unregister(DiscoveryContext... contexts) { @@ -24,7 +40,7 @@ public void unregister(DiscoveryContext... contexts) { } } - public boolean exists(String token) { - return grants.containsKey(token); + 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..fc8b407f8 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultResolverService.java @@ -0,0 +1,50 @@ +/* + * 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 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.storage.Entry; +import io.reactivex.rxjava3.core.Single; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class DefaultResolverService implements ResolverService { + + private final SecretProviderRegistry secretProviderRegistry; + + public DefaultResolverService(SecretProviderRegistry secretProviderRegistry) { + this.secretProviderRegistry = secretProviderRegistry; + } + + @Override + public Single resolve(String envId, SecretMount secretMount) { + return secretProviderRegistry + .get(envId, secretMount.provider()) + .flatMapMaybe(secretProvider -> secretProvider.resolve(secretMount)) + .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 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/DefaultRuntimeResolver.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java deleted file mode 100644 index db3ba255a..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/DefaultRuntimeResolver.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.graviteesource.services.runtimesecrets.providers; - -import io.gravitee.node.api.secrets.SecretProvider; -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.storage.Entry; -import io.reactivex.rxjava3.core.Single; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public class DefaultRuntimeResolver implements ResolverService { - - private final SecretProviderRegistry secretProviderRegistry; - - public DefaultRuntimeResolver(SecretProviderRegistry secretProviderRegistry) { - this.secretProviderRegistry = secretProviderRegistry; - } - - @Override - public Single resolve(String envId, SecretMount secretMount) { - SecretProvider secretProvider = secretProviderRegistry.get(envId, secretMount.provider()); - return secretProvider - .resolve(secretMount) - .map(secretMap -> new Entry(Entry.Type.VALUE, secretMap.asMap(), null)) - .defaultIfEmpty(new Entry(Entry.Type.NOT_FOUND, null, null)); - } - - @Override - public SecretMount toSecretMount(String envId, SecretURL secretURL) { - return secretProviderRegistry.get(envId, secretURL.provider()).fromURL(secretURL); - } -} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java deleted file mode 100644 index b08d34155..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployer.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.graviteesource.services.runtimesecrets.providers; - -import io.gravitee.node.api.secrets.runtime.providers.SecretProviderDeployer; -import java.util.Map; -import org.springframework.core.env.Environment; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public class FromConfigurationSecretProviderDeployer implements SecretProviderDeployer { - - private boolean init; - private final Environment environment; - - public FromConfigurationSecretProviderDeployer(Environment environment) { - this.environment = environment; - } - - public void init() { - if (!init) { - doInit(); - init = true; - } - } - - private void doInit() { - // TODO - } - - @Override - public void deploy(String id, String pluginId, Map config, String envId) { - // TODO - } -} 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 index a309aa278..95c0923c9 100644 --- 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 @@ -1,9 +1,26 @@ +/* + * 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.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -32,16 +49,21 @@ public void register(String id, SecretProvider provider, String envId) { * @return a secret provider * @throws SecretProviderNotFoundException if the provider is not found */ - public SecretProvider get(String envId, String id) { - return perEnv - .get(envId) - .stream() - .filter(entry -> entry.id().equals(id)) - .map(SecretProviderEntry::provider) - .findFirst() - .or(() -> Optional.ofNullable(allEnvs.get(id))) - .orElseThrow(() -> - new SecretProviderNotFoundException("Cannot find secret provider with id [%s] for environmentID [%s]".formatted(id, envId)) + public Single get(String envId, String id) { + return Maybe + .fromOptional( + perEnv + .get(envId) + .stream() + .filter(entry -> entry.id().equals(id)) + .map(SecretProviderEntry::provider) + .findFirst() + .or(() -> Optional.ofNullable(allEnvs.get(id))) + ) + .switchIfEmpty( + Single.error( + new SecretProviderNotFoundException("Cannot find secret provider with id [%s] for envId [%s]".formatted(id, envId)) + ) ); } 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..39dba85ad --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/providers/config/FromConfigurationSecretProviderDeployer.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.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, "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, provider + ".configuration"), id, envId); + environment = environment(++e); + } + // no env + if (e == 0) { + deploy(plugin, ConfigHelper.removePrefix(providerConfig, provider + ".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/spec/DefaultSpecLifecycleService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleService.java index 3ea641241..920ee1df8 100644 --- 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 @@ -1,55 +1,83 @@ +/* + * 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 com.graviteesource.services.runtimesecrets.config.Config; -import com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; 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.Entry; -import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.SingleObserver; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Action; import io.reactivex.rxjava3.schedulers.Schedulers; +import java.util.Objects; +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 DefaultSpecLifecycleService implements SpecLifecycleService { - private final EnvAwareSpecRegistry specRegistry; + private final SpecRegistry specRegistry; + private final ContextRegistry contextRegistry; private final Cache cache; private final ResolverService resolverService; + private final GrantService grantService; private final Config config; @Override public boolean shouldDeployOnTheFly(Ref ref) { - return (ref.mainType() == Ref.MainType.URI && ref.mainExpression().isLiteral() && config.allowOnTheFlySpecs()); + return (ref.mainType() == Ref.MainType.URI && ref.mainExpression().isLiteral() && config.onTheFlySpecsEnabled()); } @Override public Spec deployOnTheFly(String envId, Ref ref) { - Spec runtimeSpec = ref.toRuntimeSpec(envId); + Spec runtimeSpec = ref.asOnTheFlySpec(envId); cache.computeIfAbsent( envId, runtimeSpec.naturalId(), () -> { - specRegistry.register(envId, runtimeSpec); + specRegistry.register(runtimeSpec); SecretURL secretURL = runtimeSpec.toSecretURL(); - SecretMount mount = resolverService.toSecretMount(envId, secretURL).withoutRetries(); return resolverService - .resolve(envId, mount) - .subscribeOn(Schedulers.io()) - .onErrorResumeNext(t -> { - Entry entry = new Entry(Entry.Type.ERROR, null, t.getMessage()); - asyncResolution(runtimeSpec); - return Single.just(entry); - }) + .toSecretMount(envId, secretURL) + .map(SecretMount::withoutRetries) + .flatMap(mount -> + resolverService + .resolve(envId, mount) + .doOnSuccess(entry -> { + if (entry.type() == Entry.Type.ERROR) { + asyncResolution(runtimeSpec, config.onTheFlySpecsDelayBeforeRetryMs(), () -> {}); + } + }) + .subscribeOn(Schedulers.io()) + ) .blockingGet(); } ); @@ -57,28 +85,95 @@ public Spec deployOnTheFly(String envId, Ref ref) { return runtimeSpec; } - private Disposable asyncResolution(Spec spec) { - SecretURL secretURL = spec.toSecretURL(); - String envId = spec.envId(); - SecretMount mount = resolverService.toSecretMount(envId, secretURL); - return resolverService - .resolve(envId, mount) - .subscribeOn(Schedulers.io()) - .onErrorResumeNext(t -> Single.just(new Entry(Entry.Type.ERROR, null, t.getMessage()))) - .subscribe(entry -> cache.put(envId, spec.naturalId(), entry)); + @Override + public void deploy(Spec newSpec) { + Spec currentSpec = specRegistry.fromSpec(newSpec.envId(), newSpec); + log.info("Deploying Secret Spec: {}", newSpec); + Action afterResolve = () -> { + specRegistry.register(newSpec); + }; + boolean shouldResolve = true; + if (currentSpec != null) { + if (isNameOrLocationChanged(currentSpec, newSpec)) { + afterResolve = + () -> { + renewGrant(currentSpec, newSpec); + specRegistry.replace(currentSpec, newSpec); + if (!currentSpec.naturalId().equals(newSpec.naturalId())) { + cache.evict(newSpec.envId(), currentSpec.naturalId()); + } + }; + } else if (isACLsChange(newSpec, currentSpec)) { + renewGrant(currentSpec, newSpec); + shouldResolve = false; + } + } + + if (shouldResolve) { + asyncResolution(newSpec, 0, afterResolve); + } } - @Override - public void deploy(Spec spec) { - specRegistry.register(spec.envId(), spec); - // TODO check diff - // TODO if change clean by old name or uri - Disposable disposable = asyncResolution(spec/*, cleanupLambda*/); + private void renewGrant(Spec oldSpec, Spec newSpec) { + contextRegistry + .findBySpec(oldSpec) + .forEach(context -> { + if (grantService.isGranted(context, newSpec)) { + grantService.grant(context, newSpec); + } else { + grantService.revoke(context); + } + }); + specRegistry.replace(oldSpec, newSpec); + } + + private static boolean isACLsChange(Spec spec, Spec previousSpec) { + return !Objects.equals(previousSpec.acls(), spec.acls()); + } + + private boolean isNameOrLocationChanged(Spec oldSpec, Spec newSpec) { + record LiteSpec(String name, String uriAndKey) {} + return !Objects.equals(new LiteSpec(oldSpec.name(), oldSpec.uriAndKey()), new LiteSpec(newSpec.name(), newSpec.uriAndKey())); } @Override public void undeploy(Spec spec) { - specRegistry.unregister(spec.envId(), spec); + contextRegistry.findBySpec(spec).forEach(grantService::revoke); cache.evict(spec.envId(), spec.naturalId()); + specRegistry.unregister(spec); + } + + private void asyncResolution(Spec spec, long delayMs, @NonNull Action postResolution) { + SecretURL secretURL = spec.toSecretURL(); + String envId = spec.envId(); + resolverService + .toSecretMount(envId, secretURL) + .delay(delayMs, TimeUnit.MILLISECONDS) + .doOnSuccess(mount -> { + log.info("Resolving secret: {}", mount); + }) + .flatMap(mount -> resolverService.resolve(envId, mount).subscribeOn(Schedulers.io())) + .doOnTerminate(postResolution) + .subscribe( + new SimpleSingleObserver<>() { + @Override + public void onSuccess(@NonNull Entry entry) { + cache.put(spec.envId(), spec.naturalId(), entry); + } + + @Override + public void onError(@NonNull Throwable err) { + log.error("Async resolution failed", err); + } + } + ); + } + + private abstract static class SimpleSingleObserver implements SingleObserver { + + @Override + public void onSubscribe(@NonNull Disposable d) { + // no op + } } } 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..fd32e0039 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecRegistry.java @@ -0,0 +1,159 @@ +/* + * 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.HashMap; +import java.util.Map; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public class SpecRegistry { + + private final Map registries = new HashMap<>(); + + public void register(Spec spec) { + registry(spec.envId()).register(spec); + } + + public void unregister(Spec spec) { + registry(spec.envId()).unregister(spec); + } + + public void replace(Spec oldSpec, Spec newSpec) { + String envId = oldSpec.envId(); + synchronized (registry(envId)) { + registry(envId).unregister(oldSpec); + registry(envId).register(newSpec); + } + } + + public Spec getFromName(String envId, String name) { + return registry(envId).getFromName(name); + } + + public Spec getFromUri(String envId, String uri) { + return registry(envId).getFromUri(uri); + } + + public Spec getFromUriAndKey(String envId, String uriAndKey) { + return registry(envId).getFromUriAndKey(uriAndKey); + } + + public Spec getFromID(String envId, String id) { + return registry(envId).getFromID(id); + } + + public Spec fromSpec(String envId, Spec query) { + return registry(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 HashMap<>(); + private final Map byUri = new HashMap<>(); + private final Map byUriAndKey = new HashMap<>(); + private final Map byID = new HashMap<>(); + + 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) { + byUri.put(spec.uri(), spec); + if (spec.key() != null) { + byUriAndKey.put(spec.uriAndKey(), spec); + } + } + } + + void unregister(Spec spec) { + if (spec.id() != null) { + byID.remove(spec.id()); + } + if (spec.uri() != null) { + byUri.remove(spec.uri()); + if (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 getFromUri(String uri) { + return byUri.get(uri); + } + + Spec getFromUriAndKey(String uriAndKey) { + return byUriAndKey.get(uriAndKey); + } + + Spec getFromID(String id) { + return byID.get(id); + } + + 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 byUri.get(query.mainExpression().value()); + } + 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) { + if (query.key() != null) { + result = byUriAndKey.get(query.uriAndKey()); + } else { + result = byUri.get(query.uri()); + } + } + return result; + } + } +} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java deleted file mode 100644 index d56004af9..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/EnvAwareSpecRegistry.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.graviteesource.services.runtimesecrets.spec.registry; - -import io.gravitee.node.api.secrets.runtime.discovery.Ref; -import io.gravitee.node.api.secrets.runtime.spec.Spec; -import java.util.HashMap; -import java.util.Map; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public class EnvAwareSpecRegistry { - - private final Map registries = new HashMap<>(); - - public void register(String envId, Spec spec) { - registry(envId).register(spec); - } - - public void unregister(String envId, Spec spec) { - registry(envId).unregister(spec); - } - - public Spec getFromName(String envId, String name) { - return registry(envId).getFromName(name); - } - - public Spec getFromUri(String envId, String uri) { - return registry(envId).getFromUri(uri); - } - - public Spec getFromUriAndKey(String envId, String uriAndKey) { - return registry(envId).getFromUriAndKey(uriAndKey); - } - - public Spec getFromID(String envId, String id) { - return registry(envId).getFromID(id); - } - - public Spec fromSpec(String envId, Spec query) { - return registry(envId).fromSpec(query); - } - - public Spec fromRef(String envId, Ref query) { - return registry(envId).fromRef(query); - } - - private SpecRegistry registry(String envId) { - return registries.computeIfAbsent(envId, ignore -> new SpecRegistry()); - } -} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java deleted file mode 100644 index 0eb4bfe87..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/registry/SpecRegistry.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.graviteesource.services.runtimesecrets.spec.registry; - -import io.gravitee.node.api.secrets.runtime.discovery.Ref; -import io.gravitee.node.api.secrets.runtime.spec.Spec; -import java.util.HashMap; -import java.util.Map; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -class SpecRegistry { - - private final Map byName = new HashMap<>(); - private final Map byUri = new HashMap<>(); - private final Map byUriAndKey = new HashMap<>(); - private final Map byID = new HashMap<>(); - - 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) { - byUri.put(spec.uri(), spec); - if (spec.key() != null) { - byUriAndKey.put(spec.uriAndKey(), spec); - } - } - } - - void unregister(Spec spec) { - if (spec.id() != null) { - byID.remove(spec.id()); - } - if (spec.uri() != null) { - byUri.remove(spec.uri()); - if (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 getFromUri(String uri) { - return byUri.get(uri); - } - - Spec getFromUriAndKey(String uriAndKey) { - return byUriAndKey.get(uriAndKey); - } - - Spec getFromID(String id) { - return byID.get(id); - } - - 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.mainExpression().value()); - } - return byUri.get(query.mainExpression().value()); - } - return null; - } - - Spec fromSpec(Spec query) { - if (query.id() != null) { - return byID.get(query.id()); - } else if (query.name() != null) { - return byName.get(query.name()); - } else if (query.uri() != null) { - if (query.key() != null) { - return byUriAndKey.get(query.uriAndKey()); - } - return byUri.get(query.uri()); - } - return null; - } -} diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java deleted file mode 100644 index 63ed829ee..000000000 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/BeanFactory.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.graviteesource.services.runtimesecrets.spring; - -import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_ACL_SPECS; -import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_ON_THE_FLY_SPECS; - -import com.graviteesource.services.runtimesecrets.RuntimeSecretProcessingService; -import com.graviteesource.services.runtimesecrets.config.Config; -import com.graviteesource.services.runtimesecrets.discovery.ContextRegistry; -import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; -import com.graviteesource.services.runtimesecrets.el.ContextUpdater; -import com.graviteesource.services.runtimesecrets.grant.DefaultGrantService; -import com.graviteesource.services.runtimesecrets.grant.GrantRegistry; -import com.graviteesource.services.runtimesecrets.providers.DefaultRuntimeResolver; -import com.graviteesource.services.runtimesecrets.providers.FromConfigurationSecretProviderDeployer; -import com.graviteesource.services.runtimesecrets.providers.SecretProviderRegistry; -import com.graviteesource.services.runtimesecrets.spec.DefaultSpecLifecycleService; -import com.graviteesource.services.runtimesecrets.spec.registry.EnvAwareSpecRegistry; -import com.graviteesource.services.runtimesecrets.storage.SimpleOffHeapCache; -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 java.util.List; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -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 BeanFactory { - - @Bean - Config config( - @Value("${" + ALLOW_ON_THE_FLY_SPECS + ":true}") boolean allowRuntimeSpecs, - @Value("${" + ALLOW_EMPTY_ACL_SPECS + ":true}") boolean allowEmptyACLSpecs - ) { - return new Config(allowRuntimeSpecs, allowEmptyACLSpecs); - } - - @Bean - RuntimeSecretProcessingService runtimeSecretProcessingService( - DefinitionBrowserRegistry definitionBrowserRegistry, - SpecLifecycleService specLifecycleService, - GrantService grantService, - EnvAwareSpecRegistry specRegistry - ) { - return new RuntimeSecretProcessingService( - definitionBrowserRegistry, - new ContextRegistry(), - grantService, - specLifecycleService, - specRegistry - ); - } - - @Bean - DefinitionBrowserRegistry definitionBrowserRegistry(List browsers) { - return new DefinitionBrowserRegistry(browsers); - } - - @Bean - SpecLifecycleService secretSpecService(Cache cache, ResolverService resolverService, Config config) { - return new DefaultSpecLifecycleService(new EnvAwareSpecRegistry(), cache, resolverService, config); - } - - @Bean - Cache secretCache() { - return new SimpleOffHeapCache(); - } - - @Bean - GrantService grantService(Config config) { - return new DefaultGrantService(new GrantRegistry(), config); - } - - @Bean - EnvAwareSpecRegistry envAwareSpecRegistry() { - return new EnvAwareSpecRegistry(); - } - - @Bean - @Conditional(EnvironmentCondition.class) - SecretProviderDeployer runtimeSecretProviderDeployer(Environment environment) { - return new FromConfigurationSecretProviderDeployer(environment); - } - - @Bean - ResolverService runtimeSecretResolver() { - SecretProviderRegistry secretProviderRegistry = new SecretProviderRegistry(); - return new DefaultRuntimeResolver(secretProviderRegistry); - } - - @Bean - ContextUpdater elContextUpdater( - Cache cache, - GrantService grantService, - SpecLifecycleService specLifecycleService, - EnvAwareSpecRegistry specRegistry - ) { - return new ContextUpdater(cache, grantService, specLifecycleService, specRegistry); - } - - static class EnvironmentCondition implements Condition { - - @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata ignore) { - return context.getEnvironment().getProperty("api.secrets.allowProvidersFromConfiguration", Boolean.class, true); - } - } -} 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..8a012be29 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spring/RuntimeSecretsBeanFactory.java @@ -0,0 +1,182 @@ +/* + * 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.RuntimeSecretsProcessingService; +import com.graviteesource.services.runtimesecrets.RuntimeSecretsService; +import com.graviteesource.services.runtimesecrets.config.Config; +import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry; +import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; +import com.graviteesource.services.runtimesecrets.el.ContextUpdater; +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.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.util.List; +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.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("${" + ALLOW_EMPTY_NO_ACL_SPECS + ":true}") boolean allowEmptyACLSpecs, + @Value("${" + ON_THE_FLY_SPECS_DELAY_BEFORE_RETRY_MS + ":500}") long onTheFlySpecsDelayBeforeRetryMs + ) { + return new Config(onTheFlySpecsEnabled, onTheFlySpecsDelayBeforeRetryMs, allowEmptyACLSpecs); + } + + @Bean + RuntimeSecretsService runtimeSecretsService( + RuntimeSecretsProcessingService runtimeSecretsProcessingService, + SpecLifecycleService specLifecycleService, + SecretProviderDeployer secretProviderDeployer + ) { + return new RuntimeSecretsService(runtimeSecretsProcessingService, specLifecycleService, secretProviderDeployer); + } + + @Bean + RuntimeSecretsProcessingService runtimeSecretsProcessingService( + DefinitionBrowserRegistry definitionBrowserRegistry, + ContextRegistry contextRegistry, + SpecRegistry specRegistry, + SpecLifecycleService specLifecycleService, + GrantService grantService + ) { + return new RuntimeSecretsProcessingService( + definitionBrowserRegistry, + contextRegistry, + specRegistry, + grantService, + specLifecycleService + ); + } + + @Bean + ContextRegistry contextRegistry() { + return new DefaultContextRegistry(); + } + + @Bean + DefinitionBrowserRegistry definitionBrowserRegistry(List browsers) { + return new DefinitionBrowserRegistry(browsers); + } + + @Bean + SpecLifecycleService specLifecycleService( + ContextRegistry contextRegistry, + Cache cache, + ResolverService resolverService, + GrantService grantService, + Config config + ) { + return new DefaultSpecLifecycleService(new SpecRegistry(), contextRegistry, cache, resolverService, grantService, 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) { + return new DefaultResolverService(secretProviderRegistry); + } + + @Bean + @Conditional({ DenyConfigProviders.class }) + ResolverService runtimeSecretResolver() { + return null; + } + + @Bean + ContextUpdater elContextUpdater( + Cache cache, + GrantService grantService, + SpecLifecycleService specLifecycleService, + SpecRegistry specRegistry + ) { + return new ContextUpdater(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 index 970c85700..be36ebe20 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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..f87ed7e72 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +io.gravitee.el.TemplateEngineFactory=com.graviteesource.services.runtimesecrets.spring.SecretTemplateEngineFactory \ No newline at end of file diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java new file mode 100644 index 000000000..f63685c3e --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java @@ -0,0 +1,493 @@ +/* + * 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.discovery.DefaultContextRegistry; +import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; +import com.graviteesource.services.runtimesecrets.el.ContextUpdater; +import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine; +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.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.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.Entry; +import io.gravitee.node.secrets.plugin.mock.MockSecretProvider; +import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration; +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 RuntimeSecretsProcessingServiceTest { + + 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" + flaky: + password: iamflaky + errors: + - secret: flaky + message: huge error!!! + repeat: 1 + - secret: error + message: I am not in the mood + + """ + ); + InMemoryResource providerBarEnv = new InMemoryResource( + """ + secrets: + mySecret: + redisPassword: "tender" + ldapPassword: "regular" + + """ + ); + private SpecLifecycleService specLifeCycleService; + private Cache cache; + private SpelTemplateEngine spelTemplateEngine; + private RuntimeSecretsProcessingService cut; + + @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(true, 200, true); + GrantService grantService = new DefaultGrantService(new GrantRegistry(), config); + SpecRegistry specRegistry = new SpecRegistry(); + ContextRegistry contextRegistry = new DefaultContextRegistry(); + ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); + specLifeCycleService = new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, config); + ContextUpdater contextUpdater = new ContextUpdater(cache, grantService, specLifeCycleService, specRegistry); + spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser()); + // set up EL variables + contextUpdater.addRuntimeSecretsService(spelTemplateEngine.getTemplateContext()); + spelTemplateEngine.getTemplateContext().setVariable("uris", Map.of("redis", "/mock/mySecret:redisPassword")); + + DefinitionBrowserRegistry browserRegistry = new DefinitionBrowserRegistry(List.of(new TestDefinitionBrowser())); + cut = new RuntimeSecretsProcessingService(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(FOO_ENV_ID, name)).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(FOO_ENV_ID, name)).isPresent()); + awaitShortly().untilAsserted(() -> assertThat(cache.get(BAR_ENV_ID, name)).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, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, "/mock/mySecret")).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(FOO_ENV_ID, name)).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(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, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).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, 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, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).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, 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, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).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 spec = new Spec( + specID, + null, + "/mock/mySecret", + "redisPassword", + null, + false, + false, + null, + new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + 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 = + new Spec( + specID, + "redis-password", + "/mock/mySecret", + "redisPassword", + null, + false, + false, + null, + new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(spec); + + FakeDefinition fakeDefinition2 = new FakeDefinition("123", "<>", "<< redis-password>>"); + cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition2, Map.of("revision", "2")); + + awaitShortly() + .untilAsserted(() -> { + assertThat(cache.get(FOO_ENV_ID, "redis-password")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); + // TODO assert old secret is still there + assertThat(spelTemplateEngine.getValue(fakeDefinition2.getFirst(), String.class)).isEqualTo("fighters"); + assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition2.getSecond(), String.class)) + .isInstanceOf(SecretAccessDeniedException.class); + }); + // TODO assert old secret is evict after undeploy of revision 1 + } + + @Test + void should_continue_getting_secret_when_previous_revision_removed_unused_are_evicted() { + // PARAMTERIZED => secret are [on the fly, named, uri] + // simulate fake definition deploy + // - deploy rev1 => secret 1 + secret 2 + // - deploy rev2 => secret 1 + // - undeploy rev1 + // => secret 1 still available + } + + static class TestDefinitionBrowser implements DefinitionBrowser { + + @Override + public boolean canHandle(Object definition) { + return definition instanceof FakeDefinition; + } + + @Override + public Definition getDefinitionKindLocation(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 +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 index bed5a9e37..36507e4e4 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java similarity index 88% rename from gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java rename to gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java index ee67ec284..0f6d5eae2 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/api/model/RefTest.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/discovery/RefParserTest.java @@ -1,9 +1,23 @@ -package com.graviteesource.services.runtimesecrets.api.model; +/* + * 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 com.graviteesource.services.runtimesecrets.discovery.RefParser; import io.gravitee.node.api.secrets.runtime.discovery.Ref; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayNameGeneration; @@ -18,7 +32,7 @@ * @author GraviteeSource Team */ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class RefTest { +class RefParserTest { public static Stream okRefs() { return Stream.of( 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..a33b68466 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/el/ServiceTest.java @@ -0,0 +1,173 @@ +/* + * 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.discovery.DefaultContextRegistry; +import com.graviteesource.services.runtimesecrets.discovery.RefParser; +import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine; +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.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.secrets.plugin.mock.MockSecretProvider; +import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration; +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(true, 0, true); + this.grantService = new DefaultGrantService(new GrantRegistry(), config); + SpecRegistry specRegistry = new SpecRegistry(); + ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); + specLifeCycleService = + new DefaultSpecLifecycleService(specRegistry, new DefaultContextRegistry(), cache, resolverService, grantService, config); + ContextUpdater contextUpdater = new ContextUpdater(cache, grantService, specLifeCycleService, specRegistry); + spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser()); + // set up EL variables + contextUpdater.addRuntimeSecretsService(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(ENV_ID, naturalId)).isPresent()); + + DiscoveryContext context = new DiscoveryContext( + UUID.randomUUID(), + ENV_ID, + RefParser.parse(refAsString), + new DiscoveryLocation(new DiscoveryLocation.Definition("test", "123")) + ); + boolean authorized = grantService.isGranted(context, spec); + assertThat(authorized).isTrue(); + grantService.grant(context, spec); + + 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(ENV_ID, naturalId)).isPresent()); + boolean authorized = grantService.isGranted(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 index a996f4a40..466e4020f 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; @@ -16,6 +31,7 @@ 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.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -31,101 +47,169 @@ class DefaultGrantServiceTest { @BeforeEach void setup() { - Config config = new Config(true, true); + Config config = new Config(true, 0, true); this.cut = new DefaultGrantService(new GrantRegistry(), config); } public static Stream grants() { return Stream.of( - arguments("null spec", context("dev", "api", "123"), null, true, "no spec found"), - arguments("no acl same env", context("dev", "api", "123"), spec("dev", null), true, null), - arguments("empty acl same env", context("dev", "api", "123"), spec("dev", new ACLs(List.of(), List.of())), true, null), - arguments("no acl diff env", context("dev", "api", "123"), spec("test", null), false, null), + 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(List.of(), List.of()), null)), arguments( "def acl ok", context("dev", "api", "123"), - spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null)), - true, - null - ), - arguments( - "def acl wrong id", - context("dev", "api", "123"), - spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("456"))), null)), - false, - null + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), null) ), arguments( - "def acl wrong kind", - context("dev", "api", "123"), - spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("dict", List.of("123"))), null)), - false, - null + "def acl ok same key", + context("dev", "api", "123", "pwd"), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pwd") ), arguments( - "def acl many", + "def acl ok many", context("dev", "api", "123"), spec( "dev", new ACLs( List.of(new ACLs.DefinitionACL("dict", List.of("123")), new ACLs.DefinitionACL("api", List.of("123", "456"))), null - ) - ), - true, - null + ), + null + ) ), arguments( "plugin acl ok", context("dev", "api", "123", plugin("foo")), - spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null)))), - true, - null + spec( + "dev", + new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null))), + null + ) ), arguments( - "plugin acl ko", + "plugin acl ok many", context("dev", "api", "123", plugin("foo")), - spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("bar", null)))), - false, - null + spec( + "dev", + new ACLs( + 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 ok many", + "plugin acl only", + context("dev", "api", "123", plugin("foo")), + spec("dev", new ACLs(null, List.of(new ACLs.PluginACL("foo", null))), null) + ), + arguments( + "plugin acl only many", context("dev", "api", "123", plugin("foo")), spec( "dev", new ACLs( - List.of(new ACLs.DefinitionACL("api", List.of("123"))), + 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)) - ) - ), - true, - 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(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(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pass") + ), + arguments( + "def acl wrong id", + context("dev", "api", "123"), + spec("dev", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("456"))), null), null) + ), + arguments( + "def acl wrong kind", + context("dev", "api", "123"), + spec("dev", new ACLs(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(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, List.of(new ACLs.PluginACL("foo", null))), null) ) ); } @MethodSource("grants") @ParameterizedTest(name = "{0}") - void should_authorize(String name, DiscoveryContext context, Spec spec, boolean granted, String error) { - if (error != null) { - assertThatCode(() -> cut.authorize(context, spec)).hasMessageContaining(error); - } else { - assertThat(cut.authorize(context, spec)).isEqualTo(granted); - } + void should_grant(String name, DiscoveryContext context, Spec spec) { + assertThat(cut.isGranted(context, spec)).isTrue(); + } + + @MethodSource("denials") + @ParameterizedTest(name = "{0}") + void should_deny(String name, DiscoveryContext context, Spec spec) { + assertThat(cut.isGranted(context, spec)).isFalse(); + } + + @Test + void should_raise_error() { + DiscoveryContext context = context("dev", "api", "123"); + assertThatCode(() -> cut.isGranted(context, null)).hasMessageContaining("no spec found"); + } + + 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 new DiscoveryContext( - null, + return context( env, + kind, + id, new Ref(Ref.MainType.NAME, new Ref.Expression("secret", false), null, null, "<< secret >>"), - new DiscoveryLocation(new DiscoveryLocation.Definition(kind, id), payloads) + payloads ); } - static Spec spec(String env, ACLs acls) { - return new Spec(null, "secret", null, null, null, false, false, null, acls, env); + 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) { 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..1e7e4091f --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/providers/FromConfigurationSecretProviderDeployerTest.java @@ -0,0 +1,106 @@ +/* + * 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.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: + - enabled: true + plugin: "mock" + environments: + - "dev" + configuration: + secrets: + mySecret: + redisPassword: "foo" + ldapPassword: "bar" + - enabled: true + id: "all-env-secret-manager" + plugin: "mock" + configuration: + secrets: + my_secret: + redisPassword: "very-long-password" + ldapPassword: "also-quite-not-short-password" + - enabled: false + id: "disabled" + plugin: "mock" + configuration: {} + + """ + ); + private SecretProviderRegistry registry; + private SecretProviderPluginManager pluginManager; + 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(); + 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").test().assertError(err -> err.getMessage().contains("[mock] for envId [test]")); + registry.get("any", "disabled").test().assertError(err -> err.getMessage().contains("[disabled] for envId [any]")); + } +} 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..2b48b363f --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/spec/DefaultSpecLifecycleServiceTest.java @@ -0,0 +1,135 @@ +/* + * 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.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.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.Entry; +import io.gravitee.node.secrets.plugin.mock.MockSecretProvider; +import io.gravitee.node.secrets.plugin.mock.conf.MockSecretProviderConfiguration; +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(); + cut = + new DefaultSpecLifecycleService( + new SpecRegistry(), + new DefaultContextRegistry(), + cache, + new DefaultResolverService(registry), + new DefaultGrantService(new GrantRegistry(), new Config(true, 0, true)), + new Config(true, 0, true) + ); + } + + @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("redis-password")); + } + + @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); + assertThat(spec.uri()).isEqualTo("/mock/mySecret"); + assertThat(spec.key()).isEqualTo("redisPassword"); + Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache("/mock/mySecret")); + } + + @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("redis-password")); + cut.undeploy(spec); + assertThat(cache.get(ENV_ID, "redis-password")).isNotPresent(); + } + + private void checkInCache(String natualId) { + Optional foo = cache.get(ENV_ID, natualId); + 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 index 5b5deeba8..d440f0aff 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; 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..c5159e700 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/testsupport/SpecFixtures.java @@ -0,0 +1,70 @@ +/* + * 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(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, List.of(pluginACL)), envId); + } +} 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/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..86f3151c4 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)); } diff --git a/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml index 1862aee66..dd806ce53 100644 --- a/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml @@ -56,6 +56,17 @@ org.assertj assertj-core + + org.springframework.security + spring-security-core + test + + + org.yaml + snakeyaml + 2.2 + test + 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 index d5cebfe08..bfb9ac66b 100644 --- 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 @@ -1,44 +1,106 @@ 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.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<>(); + @Override public Maybe resolve(SecretMount secretMount) { + log.info("{}-{} resolving secret: {}", PLUGIN_ID, SECRET_PROVIDER, secretMount); + MockSecretLocation location = MockSecretLocation.fromLocation(secretMount.location()); + + // 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 = "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)); + } + } + + // normal case Map secretMap = ConfigHelper.removePrefix(configuration.getSecrets(), location.secret()); if (secretMap.isEmpty()) { + log.info("{}-{} no secrets for: {}", PLUGIN_ID, SECRET_PROVIDER, secretMount); return Maybe.empty(); } + log.info("{}-{} found secrets ({}) for: {}", PLUGIN_ID, SECRET_PROVIDER, secretMap.size(), secretMount); return Maybe.just(SecretMap.of(secretMap)); } @Override public Flowable watch(SecretMount secretMount) { - return Flowable.empty(); + 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) { - return new SecretMount(PLUGIN_ID, MockSecretLocation.fromUrl(url), url.key(), 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/MockSecretProviderConfiguration.java b/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java deleted file mode 100644 index d70ae4109..000000000 --- a/gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/MockSecretProviderConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.gravitee.node.secrets.plugin.mock; - -import io.gravitee.node.api.secrets.SecretManagerConfiguration; -import io.gravitee.node.api.secrets.util.ConfigHelper; -import java.util.Map; -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; - - @Getter - private final Map secrets; - - public MockSecretProviderConfiguration(Map config) { - this.enabled = ConfigHelper.getProperty(config, Fields.enabled, Boolean.class, false); - this.secrets = ConfigHelper.removePrefix(config, "secrets"); - } - - @Override - public boolean isEnabled() { - return enabled; - } -} 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 index 3db78ed87..1fd45ae89 100644 --- 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 @@ -2,6 +2,7 @@ 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) 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..05fa4651e --- /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,83 @@ +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 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 base = error(i); + while (config.containsKey(base + ".secret")) { + String secret = config.get(base + ".secret").toString(); + int repeat = Integer.parseInt(config.getOrDefault(base + ".repeat", String.valueOf(NO_REPEAT)).toString()); + configuredErrors.put(secret, new ConfiguredError(config.getOrDefault(base + ".message", "").toString(), repeat)); + base = error(++i); + } + + // process watch + i = 0; + base = event(i); + while (watches.containsKey(base + ".secret")) { + configuredEvents.add( + new ConfiguredEvent( + watches.get(base + ".secret").toString(), + SecretEvent.Type.valueOf(watches.getOrDefault(base + ".type", "CREATED").toString()), + ConfigHelper.removePrefix(watches, base + ".data"), + String.valueOf(watches.get(base + ".error")) + ) + ); + base = event(++i); + } + } + + private static String event(int i) { + return "events[%s]".formatted(i); + } + + private static String error(int i) { + return "errors[%s]".formatted(i); + } + + @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/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 index 57d465ea0..82ad64079 100644 --- 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 @@ -3,14 +3,18 @@ import static org.assertj.core.api.Assertions.assertThat; import io.gravitee.node.api.secrets.SecretProvider; -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.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) @@ -23,7 +27,52 @@ class MockSecretProviderTest { @BeforeEach void setup() { - Map conf = Map.of("enabled", true, "secrets.redis.password", "r3d1s", "secrets.ldap.password", "1da9"); + 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 + 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 + 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)); } @@ -38,15 +87,68 @@ void should_create_mount() { @Test void should_resolve() { SecretMount secretMountRedis = cut.fromURL(SecretURL.from("secret://mock/redis:password")); - cut.resolve(secretMountRedis).test().assertValue(SecretMap.of(Map.of("password", "r3d1s"))); + 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().assertValue(SecretMap.of(Map.of("password", "1da9"))); + 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().assertNoErrors().assertComplete(); + 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")); } } From a9f97da2a5d11a2d6092fca573579c05be385a5b Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Tue, 8 Oct 2024 11:30:50 +0200 Subject: [PATCH 04/15] fix: demo fixes --- .../api/secrets/model/SecretLocation.java | 2 + .../api/secrets/runtime/discovery/Ref.java | 2 +- .../secrets/runtime/grant/GrantService.java | 4 +- .../{RenewalPolicy.java => Resolution.java} | 6 +- .../node/api/secrets/runtime/spec/Spec.java | 2 +- gravitee-node-container/pom.xml | 12 +- .../gravitee-node-secrets-runtime/pom.xml | 33 ++-- ...sProcessingService.java => Processor.java} | 11 +- .../runtimesecrets/RuntimeSecretsService.java | 149 +++++++++++++++++- .../runtimesecrets/discovery/RefParser.java | 4 + .../services/runtimesecrets/el/Service.java | 4 +- .../SecretsTemplateVariableProvider.java} | 12 +- .../grant/DefaultGrantService.java | 30 ++-- .../providers/SecretProviderRegistry.java | 7 +- ...omConfigurationSecretProviderDeployer.java | 6 +- .../spec/DefaultSpecLifecycleService.java | 84 ++++------ .../runtimesecrets/spec/SpecRegistry.java | 26 +-- .../spring/RuntimeSecretsBeanFactory.java | 30 ++-- .../io.gravitee.el.TemplateEngineFactory | 1 + .../io.gravitee.el.TemplateVariableProvider | 1 + .../main/resources/META-INF/spring.factories | 3 +- ...ingServiceTest.java => ProcessorTest.java} | 27 ++-- .../discovery/RefParserTest.java | 2 +- .../runtimesecrets/el/ServiceTest.java | 14 +- .../grant/DefaultGrantServiceTest.java | 15 +- ...nfigurationSecretProviderDeployerTest.java | 17 +- .../src/test/resources/logback.xml | 24 +++ .../gravitee-node-secrets-service/pom.xml | 3 +- .../AbstractSecretProviderDispatcher.java | 2 +- .../gravitee-secret-provider-mock/pom.xml | 6 +- 30 files changed, 351 insertions(+), 188 deletions(-) rename gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/{RenewalPolicy.java => Resolution.java} (67%) rename gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/{RuntimeSecretsProcessingService.java => Processor.java} (94%) rename gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/{ContextUpdater.java => engine/SecretsTemplateVariableProvider.java} (73%) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateEngineFactory create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/resources/META-INF/services/io.gravitee.el.TemplateVariableProvider rename gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/{RuntimeSecretsProcessingServiceTest.java => ProcessorTest.java} (95%) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/resources/logback.xml 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/runtime/discovery/Ref.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/Ref.java index dbfd9141e..8c01d8af5 100644 --- 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 @@ -40,7 +40,7 @@ public Spec asOnTheFlySpec(String envId) { mainExpression().value(), secondaryExpression().value(), null, - mainType() == MainType.URI && mainExpression.isLiteral() && secondaryType() == null, + mainType() == MainType.URI && mainExpression.isLiteral() && (secondaryType() == null || secondaryExpression().isEL()), true, null, null, 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 index 2c6da1d9f..36bdfaf8c 100644 --- 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 @@ -11,9 +11,7 @@ public interface GrantService { Optional getGrant(String contextId); - boolean isGranted(DiscoveryContext context, Spec spec); - - void grant(DiscoveryContext context, Spec spec); + 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/spec/RenewalPolicy.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java similarity index 67% rename from gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/RenewalPolicy.java rename to gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java index 319f9e035..cee4dcfe4 100644 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/RenewalPolicy.java +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java @@ -6,10 +6,10 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record RenewalPolicy(Type type, Duration duration, Duration checkBeforeTTL) { +public record Resolution(Type type, Duration interval) { public enum Type { - NONE, - TTL, + ONCE, POLL, + WATCH, } } 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 index 59956422e..db1ce789a 100644 --- 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 @@ -19,7 +19,7 @@ public record Spec( List children, boolean usesDynamicKey, boolean isOnTheFly, - RenewalPolicy renewalPolicy, + Resolution resolution, ACLs acls, String envId ) { diff --git a/gravitee-node-container/pom.xml b/gravitee-node-container/pom.xml index c1635d0b9..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 @@ -169,11 +175,5 @@ mockito-core test - - io.gravitee.node - gravitee-node-secrets-runtime - 6.4.2 - compile - diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml index a7126e39a..31cfc1ff4 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/pom.xml @@ -23,12 +23,16 @@ io.gravitee.node gravitee-node-secrets - 6.4.2 + 6.4.4 gravitee-node-secrets-runtime Gravitee.io - Node - Secrets - Runtime + + 3.2.3 + + org.springframework @@ -38,6 +42,14 @@ 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 @@ -45,8 +57,7 @@ io.gravitee.el gravitee-expression-language - 3.2.3 - compile + ${gravitee-expression-language.version} @@ -57,7 +68,8 @@ io.gravitee.node gravitee-secret-provider-mock - 6.4.2 + ${project.version} + test org.springframework @@ -67,7 +79,7 @@ org.yaml snakeyaml - 2.2 + ${snakeyaml.version} test @@ -76,15 +88,8 @@ test - io.gravitee.apim.definition - gravitee-apim-definition-model - 4.5.0-SNAPSHOT - test - - - io.gravitee.apim.definition - gravitee-apim-definition-jackson - 4.5.0-SNAPSHOT + ch.qos.logback + logback-classic test diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java similarity index 94% rename from gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java rename to gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java index 1aa01ad1b..dd462422e 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingService.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/Processor.java @@ -38,7 +38,7 @@ */ @Slf4j @RequiredArgsConstructor -public class RuntimeSecretsProcessingService { +public class Processor { private final DefinitionBrowserRegistry definitionBrowserRegistry; private final ContextRegistry contextRegistry; @@ -81,7 +81,7 @@ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl } if (context.ref().mainExpression().isLiteral()) { - boolean granted = grantService.isGranted(context, spec); + boolean granted = grantService.grant(context, spec); if (granted) { grantService.grant(context, spec); } @@ -89,7 +89,7 @@ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl } } - static class DefaultPayloadNotifier implements DefinitionPayloadNotifier { + static final class DefaultPayloadNotifier implements DefinitionPayloadNotifier { @Getter final List contextList = new ArrayList<>(); @@ -106,6 +106,11 @@ static class DefaultPayloadNotifier implements DefinitionPayloadNotifier { @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() 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 index 5f831ab04..40408f6bb 100644 --- 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 @@ -1,13 +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; +import com.graviteesource.services.runtimesecrets.spec.SpecRegistry; 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.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.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) @@ -16,13 +42,30 @@ @RequiredArgsConstructor public class RuntimeSecretsService extends AbstractService { - private final RuntimeSecretsProcessingService runtimeSecretsProcessingService; + private final Processor processor; private final SpecLifecycleService specLifecycleService; + private final SpecRegistry specRegistry; private final SecretProviderDeployer secretProviderDeployer; + private final Environment environment; @Override protected void doStart() throws Exception { secretProviderDeployer.init(); + 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" + ) + ); } public void deploy(Spec spec) { @@ -34,6 +77,108 @@ public void undeploy(Spec spec) { } public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) { - runtimeSecretsProcessingService.onDefinitionDeploy(envId, definition, metadata); + processor.onDefinitionDeploy(envId, definition, metadata); + } + + 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 specToUndeploy = properties.getProperty("undeploySpec", ""); + String addSpec = properties.getProperty("newSpecWithACL", ""); + + if (!addSpec.isEmpty()) { + specLifecycleService.deploy( + new Spec( + "f9024ec8-ad20-4834-8962-9c9153218983", + null, + "/mock/static/uri", + "api-key", + null, + false, + false, + null, + acls(addSpec), + "DEFAULT" + ) + ); + } + if (!pluginToAdd.equals("ignore")) { + deployStaticApiKey(pluginToAdd); + } + + if (!specToUndeploy.isEmpty()) { + Spec spec = specRegistry.getFromName("DEFAULT", specToUndeploy); + specLifecycleService.undeploy(spec); + } + } + + private void deployStaticApiKey(String pluginToAdd) { + specLifecycleService.deploy( + new Spec( + "e69328d2-cdb0-4970-a94e-c521ff03f1d5", + "static-api-key", + "/mock/static/named", + "api-key", + null, + false, + false, + null, + acls(pluginToAdd), + "DEFAULT" + ) + ); + } + + private ACLs acls(String pluginToAdd) { + if (pluginToAdd.isEmpty()) { + return null; + } + return new ACLs(null, List.of(new ACLs.PluginACL(pluginToAdd, null))); } } 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 index abf53a089..0169b91d2 100644 --- 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 @@ -189,6 +189,10 @@ private String asString() { } } + 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(); 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 index 93d330afe..63b4aca36 100644 --- 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 @@ -89,7 +89,7 @@ public String fromGrantWithUri(String token, String envId, String name, String c public String fromELWithUri(String envId, String uriWithKey, String definitionKind, String definitionId) { if (uriWithKey.contains(URI_KEY_SEPARATOR)) { - RefParser.UriAndKey uriAndKey = RefParser.parseUriAndKey(uriWithKey, uriWithKey.length()); + RefParser.UriAndKey uriAndKey = RefParser.parseUriAndKey(uriWithKey); Ref ref = uriAndKey.asRef(); Spec spec = specRegistry.getFromUriAndKey(envId, uriWithKey); if (spec == null && specLifecycleService.shouldDeployOnTheFly(ref)) { @@ -108,7 +108,7 @@ public String fromELWithName(String envId, String name, String definitionKind, S } private String grantAndGet(String envId, String definitionKind, String definitionId, Spec spec, Ref ref, String naturalId, String key) { - boolean granted = grantService.isGranted( + boolean granted = grantService.grant( new DiscoveryContext(null, envId, ref, new DiscoveryLocation(new DiscoveryLocation.Definition(definitionKind, definitionId))), spec ); diff --git a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java similarity index 73% rename from gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java rename to gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java index c74b612b0..2dabe34a5 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/ContextUpdater.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/engine/SecretsTemplateVariableProvider.java @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.graviteesource.services.runtimesecrets.el; +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; @@ -27,14 +31,16 @@ * @author GraviteeSource Team */ @RequiredArgsConstructor -public class ContextUpdater { +@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; - public void addRuntimeSecretsService(TemplateContext context) { + @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/grant/DefaultGrantService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java index 96c41703a..badccbb33 100644 --- 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 @@ -20,7 +20,6 @@ import static io.gravitee.node.api.secrets.runtime.discovery.PayloadLocation.PLUGIN_KIND; import com.graviteesource.services.runtimesecrets.config.Config; -import com.graviteesource.services.runtimesecrets.errors.SecretSpecNotFoundException; 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; @@ -48,16 +47,24 @@ public class DefaultGrantService implements GrantService { private final Config config; @Override - public boolean isGranted(@Nonnull DiscoveryContext context, Spec spec) { + 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(spec.naturalId(), spec.key())); + } + return granted; + } + + private boolean isGranted(DiscoveryContext context, Spec spec) { if (spec == null) { - throw new SecretSpecNotFoundException( - "no spec found or created on-the-fly for ref [%s] in envId [%s], %s=%s".formatted( - context.ref().rawRef(), - context.envId(), - ON_THE_FLY_SPECS_ENABLED, - config.onTheFlySpecsEnabled() - ) + log.warn( + "no spec found for ref {} in envId [{}], {}={}", + context.ref().rawRef(), + context.envId(), + ON_THE_FLY_SPECS_ENABLED, + config.onTheFlySpecsEnabled() ); + return false; } if (spec.acls() == null) { if (!config.allowEmptyACLSpecs()) { @@ -75,11 +82,6 @@ public boolean isGranted(@Nonnull DiscoveryContext context, Spec spec) { return checkSpec(context).test(spec) && checkACLs(context).test(spec.acls()); } - @Override - public void grant(@Nonnull DiscoveryContext context, Spec spec) { - grantRegistry.register(context.id().toString(), new Grant(spec.naturalId(), spec.key())); - } - @Override public Optional getGrant(String contextId) { return Optional.ofNullable(grantRegistry.get(contextId)); 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 index 95c0923c9..5f1c14ac9 100644 --- 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 @@ -19,6 +19,7 @@ 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.HashMap; @@ -60,11 +61,7 @@ public Single get(String envId, String id) { .findFirst() .or(() -> Optional.ofNullable(allEnvs.get(id))) ) - .switchIfEmpty( - Single.error( - new SecretProviderNotFoundException("Cannot find secret provider with id [%s] for envId [%s]".formatted(id, envId)) - ) - ); + .switchIfEmpty(Single.just(new AbstractSecretProviderDispatcher.ErrorSecretProvider())); } public 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 index 39dba85ad..7c389d05e 100644 --- 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 @@ -55,7 +55,7 @@ public void init() { String provider = provider(i); while (apiSecrets.containsKey(provider + ".plugin")) { Map providerConfig = ConfigHelper.removePrefix(apiSecrets, provider); - if (!ConfigHelper.getProperty(providerConfig, "enabled", Boolean.class, true)) { + if (!ConfigHelper.getProperty(providerConfig, "configuration.enabled", Boolean.class, true)) { return; } String plugin = ConfigHelper.getProperty(providerConfig, "plugin", String.class); @@ -64,12 +64,12 @@ public void init() { String environment = environment(e); while (providerConfig.containsKey(environment)) { String envId = providerConfig.get(environment).toString(); - deploy(plugin, ConfigHelper.removePrefix(providerConfig, provider + ".configuration"), id, envId); + deploy(plugin, ConfigHelper.removePrefix(providerConfig, "configuration"), id, envId); environment = environment(++e); } // no env if (e == 0) { - deploy(plugin, ConfigHelper.removePrefix(providerConfig, provider + ".configuration"), id, null); + deploy(plugin, ConfigHelper.removePrefix(providerConfig, "configuration"), id, null); } provider = provider(++i); } 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 index 920ee1df8..8cad232d4 100644 --- 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 @@ -16,6 +16,7 @@ package com.graviteesource.services.runtimesecrets.spec; import com.graviteesource.services.runtimesecrets.config.Config; +import com.graviteesource.services.runtimesecrets.spec.SpecRegistry.SpecUpdate; 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; @@ -27,8 +28,6 @@ import io.gravitee.node.api.secrets.runtime.storage.Cache; import io.gravitee.node.api.secrets.runtime.storage.Entry; import io.reactivex.rxjava3.annotations.NonNull; -import io.reactivex.rxjava3.core.SingleObserver; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.functions.Action; import io.reactivex.rxjava3.schedulers.Schedulers; import java.util.Objects; @@ -86,54 +85,57 @@ public Spec deployOnTheFly(String envId, Ref ref) { } @Override - public void deploy(Spec newSpec) { - Spec currentSpec = specRegistry.fromSpec(newSpec.envId(), newSpec); - log.info("Deploying Secret Spec: {}", newSpec); - Action afterResolve = () -> { - specRegistry.register(newSpec); - }; + public void deploy(Spec spec) { + Spec currentSpec = specRegistry.fromSpec(spec.envId(), spec); + log.info("Deploying Secret Spec: {}", spec); + Action afterResolve = () -> specRegistry.register(spec); boolean shouldResolve = true; if (currentSpec != null) { - if (isNameOrLocationChanged(currentSpec, newSpec)) { + SpecUpdate update = new SpecUpdate(currentSpec, spec); + if (isNameOrLocationChanged(update)) { afterResolve = () -> { - renewGrant(currentSpec, newSpec); - specRegistry.replace(currentSpec, newSpec); - if (!currentSpec.naturalId().equals(newSpec.naturalId())) { - cache.evict(newSpec.envId(), currentSpec.naturalId()); + renewGrant(update); + specRegistry.replace(update); + if (!currentSpec.naturalId().equals(spec.naturalId())) { + cache.evict(currentSpec.envId(), currentSpec.naturalId()); } }; - } else if (isACLsChange(newSpec, currentSpec)) { - renewGrant(currentSpec, newSpec); + } else if (isACLsChange(update)) { + renewGrant(update); + specRegistry.replace(update); shouldResolve = false; } + } else { + contextRegistry.findBySpec(spec).forEach(context -> grantService.grant(context, spec)); } if (shouldResolve) { - asyncResolution(newSpec, 0, afterResolve); + asyncResolution(spec, 0, afterResolve); } } - private void renewGrant(Spec oldSpec, Spec newSpec) { + private void renewGrant(SpecUpdate update) { contextRegistry - .findBySpec(oldSpec) + .findBySpec(update.oldSpec()) .forEach(context -> { - if (grantService.isGranted(context, newSpec)) { - grantService.grant(context, newSpec); - } else { + boolean grant = grantService.grant(context, update.newSpec()); + if (!grant) { grantService.revoke(context); } }); - specRegistry.replace(oldSpec, newSpec); } - private static boolean isACLsChange(Spec spec, Spec previousSpec) { - return !Objects.equals(previousSpec.acls(), spec.acls()); + private static boolean isACLsChange(SpecUpdate update) { + return !Objects.equals(update.oldSpec().acls(), update.newSpec().acls()); } - private boolean isNameOrLocationChanged(Spec oldSpec, Spec newSpec) { + private boolean isNameOrLocationChanged(SpecUpdate update) { record LiteSpec(String name, String uriAndKey) {} - return !Objects.equals(new LiteSpec(oldSpec.name(), oldSpec.uriAndKey()), new LiteSpec(newSpec.name(), newSpec.uriAndKey())); + return !Objects.equals( + new LiteSpec(update.oldSpec().name(), update.oldSpec().uriAndKey()), + new LiteSpec(update.newSpec().name(), update.newSpec().uriAndKey()) + ); } @Override @@ -149,31 +151,11 @@ private void asyncResolution(Spec spec, long delayMs, @NonNull Action postResolu resolverService .toSecretMount(envId, secretURL) .delay(delayMs, TimeUnit.MILLISECONDS) - .doOnSuccess(mount -> { - log.info("Resolving secret: {}", mount); - }) + .doOnSuccess(mount -> log.info("Resolving secret: {}", mount)) .flatMap(mount -> resolverService.resolve(envId, mount).subscribeOn(Schedulers.io())) - .doOnTerminate(postResolution) - .subscribe( - new SimpleSingleObserver<>() { - @Override - public void onSuccess(@NonNull Entry entry) { - cache.put(spec.envId(), spec.naturalId(), entry); - } - - @Override - public void onError(@NonNull Throwable err) { - log.error("Async resolution failed", err); - } - } - ); - } - - private abstract static class SimpleSingleObserver implements SingleObserver { - - @Override - public void onSubscribe(@NonNull Disposable d) { - // no op - } + .subscribeOn(Schedulers.io()) + .doOnError(err -> log.error("Async resolution failed", err)) + .doFinally(postResolution) + .subscribe(entry -> cache.put(spec.envId(), spec.naturalId(), entry)); } } 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 index fd32e0039..5a0e13c2f 100644 --- 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 @@ -26,6 +26,8 @@ */ public class SpecRegistry { + public record SpecUpdate(Spec oldSpec, Spec newSpec) {} + private final Map registries = new HashMap<>(); public void register(Spec spec) { @@ -36,11 +38,11 @@ public void unregister(Spec spec) { registry(spec.envId()).unregister(spec); } - public void replace(Spec oldSpec, Spec newSpec) { - String envId = oldSpec.envId(); + public void replace(SpecUpdate update) { + String envId = update.oldSpec().envId(); synchronized (registry(envId)) { - registry(envId).unregister(oldSpec); - registry(envId).register(newSpec); + registry(envId).unregister(update.oldSpec()); + registry(envId).register(update.newSpec()); } } @@ -48,18 +50,10 @@ public Spec getFromName(String envId, String name) { return registry(envId).getFromName(name); } - public Spec getFromUri(String envId, String uri) { - return registry(envId).getFromUri(uri); - } - public Spec getFromUriAndKey(String envId, String uriAndKey) { return registry(envId).getFromUriAndKey(uriAndKey); } - public Spec getFromID(String envId, String id) { - return registry(envId).getFromID(id); - } - public Spec fromSpec(String envId, Spec query) { return registry(envId).fromSpec(query); } @@ -113,18 +107,10 @@ Spec getFromName(String name) { return byName.get(name); } - Spec getFromUri(String uri) { - return byUri.get(uri); - } - Spec getFromUriAndKey(String uriAndKey) { return byUriAndKey.get(uriAndKey); } - Spec getFromID(String id) { - return byID.get(id); - } - Spec fromRef(Ref query) { if (query.mainType() == Ref.MainType.NAME) { return byName.get(query.mainExpression().value()); 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 index 8a012be29..559b211e9 100644 --- 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 @@ -17,12 +17,12 @@ import static com.graviteesource.services.runtimesecrets.config.Config.*; -import com.graviteesource.services.runtimesecrets.RuntimeSecretsProcessingService; +import com.graviteesource.services.runtimesecrets.Processor; import com.graviteesource.services.runtimesecrets.RuntimeSecretsService; import com.graviteesource.services.runtimesecrets.config.Config; import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry; import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; -import com.graviteesource.services.runtimesecrets.el.ContextUpdater; +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; @@ -44,6 +44,7 @@ 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; /** @@ -64,28 +65,24 @@ Config config( @Bean RuntimeSecretsService runtimeSecretsService( - RuntimeSecretsProcessingService runtimeSecretsProcessingService, + Processor processor, SpecLifecycleService specLifecycleService, - SecretProviderDeployer secretProviderDeployer + SpecRegistry specRegistry, + SecretProviderDeployer secretProviderDeployer, + Environment environment ) { - return new RuntimeSecretsService(runtimeSecretsProcessingService, specLifecycleService, secretProviderDeployer); + return new RuntimeSecretsService(processor, specLifecycleService, specRegistry, secretProviderDeployer, environment); } @Bean - RuntimeSecretsProcessingService runtimeSecretsProcessingService( + Processor processor( DefinitionBrowserRegistry definitionBrowserRegistry, ContextRegistry contextRegistry, SpecRegistry specRegistry, SpecLifecycleService specLifecycleService, GrantService grantService ) { - return new RuntimeSecretsProcessingService( - definitionBrowserRegistry, - contextRegistry, - specRegistry, - grantService, - specLifecycleService - ); + return new Processor(definitionBrowserRegistry, contextRegistry, specRegistry, grantService, specLifecycleService); } @Bean @@ -100,13 +97,14 @@ DefinitionBrowserRegistry definitionBrowserRegistry(List brow @Bean SpecLifecycleService specLifecycleService( + SpecRegistry specRegistry, ContextRegistry contextRegistry, Cache cache, ResolverService resolverService, GrantService grantService, Config config ) { - return new DefaultSpecLifecycleService(new SpecRegistry(), contextRegistry, cache, resolverService, grantService, config); + return new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, config); } @Bean @@ -152,13 +150,13 @@ ResolverService runtimeSecretResolver() { } @Bean - ContextUpdater elContextUpdater( + SecretsTemplateVariableProvider secretsTemplateVariableProvider( Cache cache, GrantService grantService, SpecLifecycleService specLifecycleService, SpecRegistry specRegistry ) { - return new ContextUpdater(cache, grantService, specLifecycleService, specRegistry); + return new SecretsTemplateVariableProvider(cache, grantService, specLifecycleService, specRegistry); } private static final Predicate ALLOW_PROVIDERS_FROM_CONFIG = context -> 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 index f87ed7e72..7a1d11184 100644 --- 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 @@ -1 +1,2 @@ -io.gravitee.el.TemplateEngineFactory=com.graviteesource.services.runtimesecrets.spring.SecretTemplateEngineFactory \ No newline at end of file +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/RuntimeSecretsProcessingServiceTest.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java similarity index 95% rename from gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java rename to gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java index f63685c3e..412f6c892 100644 --- a/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/RuntimeSecretsProcessingServiceTest.java +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/test/java/com/graviteesource/services/runtimesecrets/ProcessorTest.java @@ -22,8 +22,8 @@ import com.graviteesource.services.runtimesecrets.config.Config; import com.graviteesource.services.runtimesecrets.discovery.DefaultContextRegistry; import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; -import com.graviteesource.services.runtimesecrets.el.ContextUpdater; 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; @@ -62,7 +62,7 @@ * @author GraviteeSource Team */ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class RuntimeSecretsProcessingServiceTest { +class ProcessorTest { public static final String FOO_ENV_ID = "foo"; public static final String BAR_ENV_ID = "bar"; @@ -95,7 +95,7 @@ class RuntimeSecretsProcessingServiceTest { private SpecLifecycleService specLifeCycleService; private Cache cache; private SpelTemplateEngine spelTemplateEngine; - private RuntimeSecretsProcessingService cut; + private Processor cut; @BeforeEach void before() { @@ -124,14 +124,19 @@ void before() { ContextRegistry contextRegistry = new DefaultContextRegistry(); ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); specLifeCycleService = new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, config); - ContextUpdater contextUpdater = new ContextUpdater(cache, grantService, specLifeCycleService, specRegistry); + SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider( + cache, + grantService, + specLifeCycleService, + specRegistry + ); spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser()); // set up EL variables - contextUpdater.addRuntimeSecretsService(spelTemplateEngine.getTemplateContext()); + secretsTemplateVariableProvider.provide(spelTemplateEngine.getTemplateContext()); spelTemplateEngine.getTemplateContext().setVariable("uris", Map.of("redis", "/mock/mySecret:redisPassword")); DefinitionBrowserRegistry browserRegistry = new DefinitionBrowserRegistry(List.of(new TestDefinitionBrowser())); - cut = new RuntimeSecretsProcessingService(browserRegistry, contextRegistry, specRegistry, grantService, specLifeCycleService); + cut = new Processor(browserRegistry, contextRegistry, specRegistry, grantService, specLifeCycleService); } @Test @@ -429,23 +434,27 @@ void should_go_from_on_the_fly_to_named_user_flow() { ); specLifeCycleService.deploy(spec); + awaitShortly() + .untilAsserted(() -> + assertThat(cache.get(FOO_ENV_ID, "redis-password")).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")); awaitShortly() .untilAsserted(() -> { assertThat(cache.get(FOO_ENV_ID, "redis-password")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); - // TODO assert old secret is still there + // TODO assert old secret is still there ??? assertThat(spelTemplateEngine.getValue(fakeDefinition2.getFirst(), String.class)).isEqualTo("fighters"); assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition2.getSecond(), String.class)) .isInstanceOf(SecretAccessDeniedException.class); }); - // TODO assert old secret is evict after undeploy of revision 1 + // TODO assert on the fly secret is evicted after undeploy of revision 1 } @Test void should_continue_getting_secret_when_previous_revision_removed_unused_are_evicted() { - // PARAMTERIZED => secret are [on the fly, named, uri] // simulate fake definition deploy // - deploy rev1 => secret 1 + secret 2 // - deploy rev2 => secret 1 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 index 0f6d5eae2..a7bb81252 100644 --- 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 @@ -135,7 +135,7 @@ void should_parse(String name, String given, Ref expected) { @Test void should_parse_uri_and_key() { String expression = "/provider/secret:password"; - assertThat(RefParser.parseUriAndKey(expression, expression.length())) + 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 index a33b68466..d78e33b41 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -92,10 +93,15 @@ void before() { ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); specLifeCycleService = new DefaultSpecLifecycleService(specRegistry, new DefaultContextRegistry(), cache, resolverService, grantService, config); - ContextUpdater contextUpdater = new ContextUpdater(cache, grantService, specLifeCycleService, specRegistry); + SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider( + cache, + grantService, + specLifeCycleService, + specRegistry + ); spelTemplateEngine = new SecretSpelTemplateEngine(new SpelExpressionParser()); // set up EL variables - contextUpdater.addRuntimeSecretsService(spelTemplateEngine.getTemplateContext()); + 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")); @@ -129,7 +135,7 @@ void should_call_service_using_fromGrant( RefParser.parse(refAsString), new DiscoveryLocation(new DiscoveryLocation.Definition("test", "123")) ); - boolean authorized = grantService.isGranted(context, spec); + boolean authorized = grantService.grant(context, spec); assertThat(authorized).isTrue(); grantService.grant(context, spec); @@ -158,7 +164,7 @@ void should_call_service_using_fromELWith(String test, String specName, String n Spec spec = new Spec(null, specName, "/mock/mySecret", "redisPassword", null, false, false, null, null, ENV_ID); specLifeCycleService.deploy(spec); shortAwait().untilAsserted(() -> assertThat(cache.get(ENV_ID, naturalId)).isPresent()); - boolean authorized = grantService.isGranted(context, spec); + boolean authorized = grantService.grant(context, spec); assertThat(authorized).isTrue(); grantService.grant(context, spec); } 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 index 466e4020f..87e9330e3 100644 --- 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 @@ -16,7 +16,6 @@ package com.graviteesource.services.runtimesecrets.grant; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.params.provider.Arguments.arguments; import com.graviteesource.services.runtimesecrets.config.Config; @@ -31,7 +30,6 @@ 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.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -156,26 +154,21 @@ public static Stream denials() { "plugin acl only ko", context("dev", "api", "123", plugin("bar")), spec("dev", new ACLs(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.isGranted(context, spec)).isTrue(); + assertThat(cut.grant(context, spec)).isTrue(); } @MethodSource("denials") @ParameterizedTest(name = "{0}") void should_deny(String name, DiscoveryContext context, Spec spec) { - assertThat(cut.isGranted(context, spec)).isFalse(); - } - - @Test - void should_raise_error() { - DiscoveryContext context = context("dev", "api", "123"); - assertThatCode(() -> cut.isGranted(context, null)).hasMessageContaining("no spec found"); + assertThat(cut.grant(context, spec)).isFalse(); } static DiscoveryContext context(String env, String kind, String id, String key, PayloadLocation... payloads) { 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 index 1e7e4091f..cceb4fb7a 100644 --- 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 @@ -43,32 +43,31 @@ class FromConfigurationSecretProviderDeployerTest { api: secrets: providers: - - enabled: true - plugin: "mock" + - plugin: "mock" environments: - "dev" configuration: + enabled: true secrets: mySecret: redisPassword: "foo" ldapPassword: "bar" - - enabled: true - id: "all-env-secret-manager" + - id: "all-env-secret-manager" plugin: "mock" configuration: + enabled: true secrets: my_secret: redisPassword: "very-long-password" ldapPassword: "also-quite-not-short-password" - - enabled: false - id: "disabled" + - id: "disabled" plugin: "mock" - configuration: {} + configuration: + enabled: false """ ); private SecretProviderRegistry registry; - private SecretProviderPluginManager pluginManager; private FromConfigurationSecretProviderDeployer cut; @BeforeEach @@ -78,7 +77,7 @@ void before() { MockEnvironment mockEnvironment = new MockEnvironment(); mockEnvironment.getPropertySources().addFirst(new MapPropertySource("test", new LinkedHashMap(yaml.getObject()))); registry = new SecretProviderRegistry(); - pluginManager = PluginManagerHelper.newPluginManagerWithMockPlugin(); + SecretProviderPluginManager pluginManager = PluginManagerHelper.newPluginManagerWithMockPlugin(); cut = new FromConfigurationSecretProviderDeployer(mockEnvironment, registry, pluginManager); } 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-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 86f3151c4..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 @@ -99,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-secret-provider-mock/pom.xml b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml index dd806ce53..b1f15f7bf 100644 --- a/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml +++ b/gravitee-node-secrets/gravitee-secret-provider-mock/pom.xml @@ -23,11 +23,11 @@ io.gravitee.node gravitee-node-secrets - 6.4.2 + 6.4.4 gravitee-secret-provider-mock - Gravitee.io - Node - Secrets - Mock Provider + Gravitee.io - Plugin - Secret Provider - Mock 3.7.1 @@ -64,7 +64,7 @@ org.yaml snakeyaml - 2.2 + ${snakeyaml.version} test From 3b6aa1462cfdac41deb3543b7cefac73f94515d8 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Thu, 10 Oct 2024 19:54:08 +0200 Subject: [PATCH 05/15] fix: renewals --- .../io/gravitee/node/api/license/License.java | 8 +- .../node/api/secrets/model/SecretMap.java | 14 +- .../runtime/discovery/ContextRegistry.java | 1 + .../runtime/discovery/DefinitionBrowser.java | 2 +- .../api/secrets/runtime/spec/Resolution.java | 3 +- .../node/api/secrets/runtime/spec/Spec.java | 11 +- .../node/license/DefaultLicenseManager.java | 6 +- .../license/DefaultLicenseFactoryTest.java | 2 +- .../services/runtimesecrets/Processor.java | 49 ++++++- .../runtimesecrets/RuntimeSecretsService.java | 27 ++++ .../runtimesecrets/config/Config.java | 10 +- .../runtimesecrets/config/OnTheFlySpecs.java | 24 ++++ .../runtimesecrets/config/Renewal.java | 24 ++++ .../discovery/DefaultContextRegistry.java | 21 ++- .../services/runtimesecrets/el/Service.java | 2 +- .../grant/DefaultGrantService.java | 10 +- ...omConfigurationSecretProviderDeployer.java | 5 +- .../renewal/RenewalService.java | 117 ++++++++++++++++ .../spec/DefaultSpecLifecycleService.java | 29 ++-- .../runtimesecrets/spec/SpecRegistry.java | 43 +++--- .../runtimesecrets/spec/SpecUpdate.java | 24 ++++ .../spring/RuntimeSecretsBeanFactory.java | 42 +++++- .../runtimesecrets/ProcessorTest.java | 130 +++++++++++++++--- .../runtimesecrets/el/ServiceTest.java | 17 ++- .../grant/DefaultGrantServiceTest.java | 5 +- ...nfigurationSecretProviderDeployerTest.java | 13 +- .../spec/DefaultSpecLifecycleServiceTest.java | 11 +- .../plugin/mock/MockSecretProvider.java | 35 ++++- .../conf/MockSecretProviderConfiguration.java | 53 +++++-- .../secrets/plugin/mock/conf/Renewal.java | 10 ++ .../plugin/mock/MockSecretProviderTest.java | 45 +++++- 31 files changed, 668 insertions(+), 125 deletions(-) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/OnTheFlySpecs.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Renewal.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/spec/SpecUpdate.java create mode 100644 gravitee-node-secrets/gravitee-secret-provider-mock/src/main/java/io/gravitee/node/secrets/plugin/mock/conf/Renewal.java 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/SecretMap.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/model/SecretMap.java index 166a0a925..6b3105a04 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,10 +35,10 @@ 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); @@ -65,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 @@ -88,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 @@ -114,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); 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 index 6df3d92d1..5163125f4 100644 --- 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 @@ -11,4 +11,5 @@ 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/DefinitionBrowser.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/discovery/DefinitionBrowser.java index 592916375..44e867211 100644 --- 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 @@ -9,7 +9,7 @@ public interface DefinitionBrowser { boolean canHandle(Object definition); - Definition getDefinitionKindLocation(T definition, Map metadata); + 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/spec/Resolution.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Resolution.java index cee4dcfe4..04642d6b0 100644 --- 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 @@ -6,10 +6,9 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record Resolution(Type type, Duration interval) { +public record Resolution(Type type, Duration pollInterval) { public enum Type { ONCE, POLL, - WATCH, } } 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 index db1ce789a..5714e22b1 100644 --- 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 @@ -14,8 +14,8 @@ public record Spec( String id, String name, - String uri, - String key, + String uri, // /vault/secrets/passwords + String key, // 1/ redis, 2/ ldap List children, boolean usesDynamicKey, boolean isOnTheFly, @@ -54,4 +54,11 @@ public String naturalId() { } 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-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-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 index dd462422e..702bd1a46 100644 --- 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 @@ -57,14 +57,13 @@ public class Processor { * @param the kind of subject */ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) { - Optional> browser = definitionBrowserRegistry.findBrowser(definition); + Optional> browser = getDefinitionBrowser(definition); if (browser.isEmpty()) { - log.info("No definition browser found for kind [{}]", definition.getClass()); return; } DefinitionBrowser definitionBrowser = browser.get(); - Definition rootDefinition = definitionBrowser.getDefinitionKindLocation(definition, metadata); + Definition rootDefinition = definitionBrowser.getDefinitionLocation(definition, metadata); log.info("Finding secret in definition: {}", rootDefinition); @@ -89,6 +88,50 @@ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl } } + 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 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 index 40408f6bb..d5ef04cc7 100644 --- 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 @@ -15,10 +15,12 @@ */ package com.graviteesource.services.runtimesecrets; +import com.graviteesource.services.runtimesecrets.renewal.RenewalService; import com.graviteesource.services.runtimesecrets.spec.SpecRegistry; 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; @@ -26,6 +28,7 @@ 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; @@ -46,11 +49,17 @@ public class RuntimeSecretsService extends AbstractService void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl processor.onDefinitionDeploy(envId, definition, metadata); } + public void onDefinitionUnDeploy(String envId, @Nonnull T definition, @Nullable Map metadata) { + processor.onDefinitionUnDeploy(envId, definition, metadata); + } + private void startWatch(String directory) { if (directory == null) { return; 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 index b7622c648..0620b230e 100644 --- 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 @@ -19,10 +19,14 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record Config(boolean onTheFlySpecsEnabled, long onTheFlySpecsDelayBeforeRetryMs, boolean allowEmptyACLSpecs) { +public record Config(boolean denySpecWithoutACLs, OnTheFlySpecs onTheFlySpecs, Renewal renewal) { public static final String CONFIG_PREFIX = "api.secrets"; - public static final String ALLOW_EMPTY_NO_ACL_SPECS = CONFIG_PREFIX + ".allowNoACLsSpecs"; + public static final String DENY_SPEC_WITHOUT_ACLS = CONFIG_PREFIX + ".denySpecWithoutACLs"; public static final String ON_THE_FLY_SPECS_ENABLED = CONFIG_PREFIX + ".onTheFlySpecs.enabled"; - public static final String ON_THE_FLY_SPECS_DELAY_BEFORE_RETRY_MS = CONFIG_PREFIX + ".onTheFlySpecs.delayBeforeRetryMs"; + 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 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/discovery/DefaultContextRegistry.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/DefaultContextRegistry.java index 9e620df12..2cb907262 100644 --- 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 @@ -47,6 +47,11 @@ public List getByDefinition(String envId, Definition definitio 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()); } @@ -86,7 +91,21 @@ public List findBySpec(Spec spec) { } public List getByDefinition(String envId, Definition definition) { - return (List) byDefinitionSpec.get(definition); + 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()) { + byName.remove(context.ref().mainExpression().value(), context); + } + if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { + byUri.remove(context.ref().mainExpression().value(), context); + if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { + byUriAndKey.remove(context.ref().uriAndKey(), context); + } + } + byDefinitionSpec.put(definition, context); } } } 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 index 63b4aca36..fe9ceb1d7 100644 --- 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 @@ -136,7 +136,7 @@ private Result toResult(Entry entry, String key) { if (secret != null) { result = new Result(Result.Type.VALUE, secret.asString()); } else { - result = new Result(Result.Type.KEY_NOT_FOUND, "key [%s] not found"); + result = new Result(Result.Type.KEY_NOT_FOUND, "key [%s] not found".formatted(key)); } } case EMPTY -> { 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 index badccbb33..bf1b9dd5b 100644 --- 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 @@ -15,7 +15,7 @@ */ package com.graviteesource.services.runtimesecrets.grant; -import static com.graviteesource.services.runtimesecrets.config.Config.ALLOW_EMPTY_NO_ACL_SPECS; +import static com.graviteesource.services.runtimesecrets.config.Config.DENY_SPEC_WITHOUT_ACLS; 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; @@ -62,16 +62,16 @@ private boolean isGranted(DiscoveryContext context, Spec spec) { context.ref().rawRef(), context.envId(), ON_THE_FLY_SPECS_ENABLED, - config.onTheFlySpecsEnabled() + config.onTheFlySpecs().enabled() ); return false; } if (spec.acls() == null) { - if (!config.allowEmptyACLSpecs()) { + if (config.denySpecWithoutACLs()) { log.warn( - "secret spec for ref [{}] is not granted because is does not contains ACLs and this is not allowed. see: {}", + "secret spec for ref [{}] not granted because secrets requires ACLs. see conf: {}", context.ref().rawRef(), - ALLOW_EMPTY_NO_ACL_SPECS + DENY_SPEC_WITHOUT_ACLS ); return false; } else { 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 index 7c389d05e..b1d30f817 100644 --- 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 @@ -88,8 +88,9 @@ public void deploy(String pluginId, Map configurationProperties, Class configurationClass1 = factory.getClass().getClassLoader().loadClass(configurationClass.getName()); try { @SuppressWarnings("unchecked") - Constructor constructor = - (Constructor) configurationClass1.getDeclaredConstructor(Map.class); + Constructor constructor = (Constructor) configurationClass1.getDeclaredConstructor( + Map.class + ); config = constructor.newInstance(configurationProperties); } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new SecretManagerConfigurationException( 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..0300a1d47 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java @@ -0,0 +1,117 @@ +/* + * 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.spec.SpecUpdate; +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.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import java.time.Instant; +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) { + if (specUpdate.newSpec().hasResolutionType(Resolution.Type.POLL)) { + synchronized (specsToRenew) { + Spec oldSpec = specUpdate.oldSpec(); + if (oldSpec != null) { + specsToRenew.remove(oldSpec); + } + 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) + ) + // todo group-by + .concatMapSingle(spec -> { + log.info("Renewing secret for spec {}", spec); + return resolverService + .toSecretMount(spec.envId(), spec.toSecretURL()) + .flatMap(secretMount -> resolverService.resolve(spec.envId(), secretMount)) + .doOnSuccess(entry -> { + // todo update partial + setupNextCheck(spec); + cache.put(spec.envId(), spec.naturalId(), entry); + }); + }) + .subscribe(); + } + + public void stop() { + if (poller != null) { + poller.dispose(); + } + } + + private void setupNextCheck(Spec spec) { + specsToRenew.put(spec, Instant.now().plus(spec.resolution().pollInterval())); + } +} 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 index 8cad232d4..93a035be2 100644 --- 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 @@ -16,7 +16,7 @@ package com.graviteesource.services.runtimesecrets.spec; import com.graviteesource.services.runtimesecrets.config.Config; -import com.graviteesource.services.runtimesecrets.spec.SpecRegistry.SpecUpdate; +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; @@ -29,7 +29,7 @@ import io.gravitee.node.api.secrets.runtime.storage.Entry; import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.functions.Action; -import io.reactivex.rxjava3.schedulers.Schedulers; +import java.time.Duration; import java.util.Objects; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @@ -48,34 +48,35 @@ public class DefaultSpecLifecycleService implements SpecLifecycleService { 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.onTheFlySpecsEnabled()); + return (ref.mainType() == Ref.MainType.URI && ref.mainExpression().isLiteral() && config.onTheFlySpecs().enabled()); } @Override public Spec deployOnTheFly(String envId, Ref ref) { Spec runtimeSpec = ref.asOnTheFlySpec(envId); + specRegistry.register(runtimeSpec); cache.computeIfAbsent( envId, runtimeSpec.naturalId(), () -> { - specRegistry.register(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) { - asyncResolution(runtimeSpec, config.onTheFlySpecsDelayBeforeRetryMs(), () -> {}); + asyncResolution(runtimeSpec, config.onTheFlySpecs().onErrorRetryAfter(), () -> {}); } }) - .subscribeOn(Schedulers.io()) ) .blockingGet(); } @@ -86,7 +87,7 @@ public Spec deployOnTheFly(String envId, Ref ref) { @Override public void deploy(Spec spec) { - Spec currentSpec = specRegistry.fromSpec(spec.envId(), spec); + Spec currentSpec = specRegistry.fromSpec(spec); log.info("Deploying Secret Spec: {}", spec); Action afterResolve = () -> specRegistry.register(spec); boolean shouldResolve = true; @@ -106,12 +107,14 @@ public void deploy(Spec spec) { specRegistry.replace(update); shouldResolve = false; } + renewalService.onSpec(update); } else { + renewalService.onSpec(spec); contextRegistry.findBySpec(spec).forEach(context -> grantService.grant(context, spec)); } if (shouldResolve) { - asyncResolution(spec, 0, afterResolve); + asyncResolution(spec, Duration.ZERO, afterResolve); } } @@ -141,19 +144,19 @@ record LiteSpec(String name, String uriAndKey) {} @Override public void undeploy(Spec spec) { contextRegistry.findBySpec(spec).forEach(grantService::revoke); - cache.evict(spec.envId(), spec.naturalId()); specRegistry.unregister(spec); + cache.evict(spec.envId(), spec.naturalId()); + renewalService.onDelete(spec); } - private void asyncResolution(Spec spec, long delayMs, @NonNull Action postResolution) { + private void asyncResolution(Spec spec, Duration delay, @NonNull Action postResolution) { SecretURL secretURL = spec.toSecretURL(); String envId = spec.envId(); resolverService .toSecretMount(envId, secretURL) - .delay(delayMs, TimeUnit.MILLISECONDS) + .delay(delay.toMillis(), TimeUnit.MILLISECONDS) .doOnSuccess(mount -> log.info("Resolving secret: {}", mount)) - .flatMap(mount -> resolverService.resolve(envId, mount).subscribeOn(Schedulers.io())) - .subscribeOn(Schedulers.io()) + .flatMap(mount -> resolverService.resolve(envId, mount)) .doOnError(err -> log.error("Async resolution failed", err)) .doFinally(postResolution) .subscribe(entry -> cache.put(spec.envId(), spec.naturalId(), entry)); 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 index 5a0e13c2f..3782d7fec 100644 --- 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 @@ -19,16 +19,17 @@ import io.gravitee.node.api.secrets.runtime.spec.Spec; import java.util.HashMap; 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 { - public record SpecUpdate(Spec oldSpec, Spec newSpec) {} - - private final Map registries = new HashMap<>(); + private final Map registries = new ConcurrentHashMap<>(); public void register(Spec spec) { registry(spec.envId()).register(spec); @@ -54,14 +55,18 @@ public Spec getFromUriAndKey(String envId, String uriAndKey) { return registry(envId).getFromUriAndKey(uriAndKey); } - public Spec fromSpec(String envId, Spec query) { - return registry(envId).fromSpec(query); + public Spec fromSpec(Spec query) { + return registry(query.envId()).fromSpec(query); } public Spec fromRef(String envId, Ref query) { return registry(envId).fromRef(query); } + public Spec fromId(String envId, String id) { + return registry(envId).fromId(id); + } + private Registry registry(String envId) { return registries.computeIfAbsent(envId, ignore -> new Registry()); } @@ -69,7 +74,6 @@ private Registry registry(String envId) { private static class Registry { private final Map byName = new HashMap<>(); - private final Map byUri = new HashMap<>(); private final Map byUriAndKey = new HashMap<>(); private final Map byID = new HashMap<>(); @@ -80,11 +84,8 @@ void register(Spec spec) { if (spec.name() != null) { byName.put(spec.name(), spec); } - if (spec.uri() != null) { - byUri.put(spec.uri(), spec); - if (spec.key() != null) { - byUriAndKey.put(spec.uriAndKey(), spec); - } + if (spec.uri() != null && spec.key() != null) { + byUriAndKey.put(spec.uriAndKey(), spec); } } @@ -92,11 +93,8 @@ void unregister(Spec spec) { if (spec.id() != null) { byID.remove(spec.id()); } - if (spec.uri() != null) { - byUri.remove(spec.uri()); - if (spec.key() != null) { - byUriAndKey.remove(spec.uriAndKey(), spec); - } + if (spec.uri() != null && spec.key() != null) { + byUriAndKey.remove(spec.uriAndKey(), spec); } if (spec.name() != null) { byName.remove(spec.name()); @@ -119,7 +117,6 @@ Spec fromRef(Ref query) { if (query.secondaryType() == Ref.SecondaryType.KEY) { return byUriAndKey.get(query.uriAndKey()); } - return byUri.get(query.mainExpression().value()); } return null; } @@ -132,14 +129,14 @@ Spec fromSpec(Spec query) { if (result == null && query.name() != null) { result = byName.get(query.name()); } - if (result == null && query.uri() != null) { - if (query.key() != null) { - result = byUriAndKey.get(query.uriAndKey()); - } else { - result = byUri.get(query.uri()); - } + if (result == null && query.uri() != null && query.key() != null) { + result = byUriAndKey.get(query.uriAndKey()); } return result; } + + Spec fromId(String id) { + return byID.get(id); + } } } 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 index 559b211e9..9853ebc21 100644 --- 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 @@ -20,6 +20,8 @@ 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.discovery.DefaultContextRegistry; import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; import com.graviteesource.services.runtimesecrets.el.engine.SecretsTemplateVariableProvider; @@ -28,6 +30,7 @@ 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; @@ -39,7 +42,9 @@ 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.*; @@ -56,11 +61,22 @@ public class RuntimeSecretsBeanFactory { @Bean Config config( + @Value("${" + DENY_SPEC_WITHOUT_ACLS + ":false}") boolean denySpecWithoutACLs, @Value("${" + ON_THE_FLY_SPECS_ENABLED + ":true}") boolean onTheFlySpecsEnabled, - @Value("${" + ALLOW_EMPTY_NO_ACL_SPECS + ":true}") boolean allowEmptyACLSpecs, - @Value("${" + ON_THE_FLY_SPECS_DELAY_BEFORE_RETRY_MS + ":500}") long onTheFlySpecsDelayBeforeRetryMs + @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 ) { - return new Config(onTheFlySpecsEnabled, onTheFlySpecsDelayBeforeRetryMs, allowEmptyACLSpecs); + return new Config( + denySpecWithoutACLs, + new OnTheFlySpecs( + onTheFlySpecsEnabled, + Duration.of(onTheFlySpecsOnErrorRetryAfterDelay, onTheFlySpecsOnErrorRetryAfterUnit.toChronoUnit()) + ), + new Renewal(renewalEnabled, Duration.of(renewalCheckDelay, renewalCheckUnit.toChronoUnit())) + ); } @Bean @@ -69,9 +85,17 @@ RuntimeSecretsService runtimeSecretsService( SpecLifecycleService specLifecycleService, SpecRegistry specRegistry, SecretProviderDeployer secretProviderDeployer, + RenewalService renewalService, Environment environment ) { - return new RuntimeSecretsService(processor, specLifecycleService, specRegistry, secretProviderDeployer, environment); + return new RuntimeSecretsService( + processor, + specLifecycleService, + specRegistry, + secretProviderDeployer, + renewalService, + environment + ); } @Bean @@ -95,6 +119,11 @@ DefinitionBrowserRegistry definitionBrowserRegistry(List brow return new DefinitionBrowserRegistry(browsers); } + @Bean + RenewalService renewalService(ResolverService resolverService, Cache cache, Config config) { + return new RenewalService(resolverService, cache, config); + } + @Bean SpecLifecycleService specLifecycleService( SpecRegistry specRegistry, @@ -102,9 +131,10 @@ SpecLifecycleService specLifecycleService( Cache cache, ResolverService resolverService, GrantService grantService, - Config config + Config config, + RenewalService renewalService ) { - return new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, config); + return new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, renewalService, config); } @Bean 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 index 412f6c892..111f682dc 100644 --- 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 @@ -20,6 +20,8 @@ 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.discovery.DefaultContextRegistry; import com.graviteesource.services.runtimesecrets.discovery.DefinitionBrowserRegistry; import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine; @@ -30,6 +32,7 @@ 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; @@ -39,12 +42,14 @@ 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.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; @@ -72,15 +77,25 @@ class ProcessorTest { 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 - + - 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( @@ -96,6 +111,7 @@ class ProcessorTest { private Cache cache; private SpelTemplateEngine spelTemplateEngine; private Processor cut; + private RenewalService renewalService; @BeforeEach void before() { @@ -118,12 +134,15 @@ void before() { ); cache = new SimpleOffHeapCache(); - Config config = new Config(true, 200, true); + Config config = new Config(false, new OnTheFlySpecs(true, Duration.ofMillis(200)), 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); - specLifeCycleService = new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, config); + + renewalService = new RenewalService(resolverService, cache, config); + specLifeCycleService = + new DefaultSpecLifecycleService(specRegistry, contextRegistry, cache, resolverService, grantService, renewalService, config); SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider( cache, grantService, @@ -418,6 +437,9 @@ void should_go_from_on_the_fly_to_named_user_flow() { .isInstanceOf(SecretAccessDeniedException.class); }); + // TODO, this needs to be refined. The cache key should the Id. CacheKey.from(spec) ? + // This would let the previous on the fly untouched and not evicted, then when undeployed => remove and evict + // create spec to limit sage spec = new Spec( @@ -445,21 +467,85 @@ void should_go_from_on_the_fly_to_named_user_flow() { awaitShortly() .untilAsserted(() -> { assertThat(cache.get(FOO_ENV_ID, "redis-password")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); - // TODO assert old secret is still there ??? assertThat(spelTemplateEngine.getValue(fakeDefinition2.getFirst(), String.class)).isEqualTo("fighters"); assertThatCode(() -> spelTemplateEngine.getValue(fakeDefinition2.getSecond(), String.class)) .isInstanceOf(SecretAccessDeniedException.class); }); - // TODO assert on the fly secret is evicted after undeploy of revision 1 } @Test - void should_continue_getting_secret_when_previous_revision_removed_unused_are_evicted() { - // simulate fake definition deploy - // - deploy rev1 => secret 1 + secret 2 - // - deploy rev2 => secret 1 - // - undeploy rev1 - // => secret 1 still available + 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(FOO_ENV_ID, "/mock/secondSecret")).isNotPresent(); + } + + @Test + void should_renew_secrets() { + renewalService.start(); + + specLifeCycleService.deploy( + new Spec( + "123456", + "rotating", + "/mock/rotating", + "password", + null, + false, + false, + new Resolution(Resolution.Type.POLL, Duration.ofSeconds(1)), + null, + FOO_ENV_ID + ) + ); + + awaitShortly() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(cache.get(FOO_ENV_ID, "rotating")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); + }); + + FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>"); + cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1")); + awaitShortly() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat(cache.get(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 { @@ -470,7 +556,7 @@ public boolean canHandle(Object definition) { } @Override - public Definition getDefinitionKindLocation(FakeDefinition definition, Map metadata) { + public Definition getDefinitionLocation(FakeDefinition definition, Map metadata) { return new Definition("test", definition.getId(), Optional.of(metadata.get("revision"))); } @@ -492,11 +578,11 @@ public void findPayloads(FakeDefinition definition, DefinitionPayloadNotifier no ConditionFactory awaitShortly() { return await().pollDelay(0, TimeUnit.MILLISECONDS).pollInterval(20, TimeUnit.MILLISECONDS).atMost(100, TimeUnit.MILLISECONDS); } -} -@Data -@AllArgsConstructor -class FakeDefinition { + @Data + @AllArgsConstructor + static class FakeDefinition { - private String id, first, second; + private String id, first, second; + } } 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 index d78e33b41..1515a71c8 100644 --- 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 @@ -19,6 +19,8 @@ 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.discovery.DefaultContextRegistry; import com.graviteesource.services.runtimesecrets.discovery.RefParser; import com.graviteesource.services.runtimesecrets.el.engine.SecretSpelTemplateEngine; @@ -27,6 +29,7 @@ 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; @@ -41,6 +44,7 @@ import io.gravitee.node.api.secrets.runtime.storage.Cache; 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; @@ -87,12 +91,21 @@ void before() { null ); cache = new SimpleOffHeapCache(); - Config config = new Config(true, 0, true); + Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); this.grantService = new DefaultGrantService(new GrantRegistry(), config); SpecRegistry specRegistry = new SpecRegistry(); ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); + RenewalService renewalService = new RenewalService(resolverService, cache, config); specLifeCycleService = - new DefaultSpecLifecycleService(specRegistry, new DefaultContextRegistry(), cache, resolverService, grantService, config); + new DefaultSpecLifecycleService( + specRegistry, + new DefaultContextRegistry(), + cache, + resolverService, + grantService, + renewalService, + config + ); SecretsTemplateVariableProvider secretsTemplateVariableProvider = new SecretsTemplateVariableProvider( cache, grantService, 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 index 87e9330e3..48fa81375 100644 --- 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 @@ -19,12 +19,15 @@ 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 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; @@ -45,7 +48,7 @@ class DefaultGrantServiceTest { @BeforeEach void setup() { - Config config = new Config(true, 0, true); + Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); this.cut = new DefaultGrantService(new GrantRegistry(), config); } 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 index cceb4fb7a..49f932277 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -99,7 +100,15 @@ void should_load_providers() { .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").test().assertError(err -> err.getMessage().contains("[mock] for envId [test]")); - registry.get("any", "disabled").test().assertError(err -> err.getMessage().contains("[disabled] for envId [any]")); + 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 index 2b48b363f..62afe814f 100644 --- 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 @@ -18,12 +18,15 @@ 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.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; @@ -32,6 +35,7 @@ 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; @@ -79,14 +83,17 @@ void before() { null ); cache = new SimpleOffHeapCache(); + Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); + RenewalService renewalService = new RenewalService(null, cache, config); cut = new DefaultSpecLifecycleService( new SpecRegistry(), new DefaultContextRegistry(), cache, new DefaultResolverService(registry), - new DefaultGrantService(new GrantRegistry(), new Config(true, 0, true)), - new Config(true, 0, true) + new DefaultGrantService(new GrantRegistry(), config), + renewalService, + config ); } 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 index bfb9ac66b..bd3df1f5b 100644 --- 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 @@ -11,6 +11,7 @@ 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; @@ -36,12 +37,12 @@ public class MockSecretProvider implements SecretProvider { private final MockSecretProviderConfiguration configuration; Map errorReturned = new ConcurrentHashMap<>(); + Map renewalReturned = new ConcurrentHashMap<>(); @Override public Maybe resolve(SecretMount secretMount) { - log.info("{}-{} resolving secret: {}", PLUGIN_ID, SECRET_PROVIDER, 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()); @@ -60,22 +61,44 @@ public Maybe resolve(SecretMount secretMount) { ); return resolve(secretMount); } - String message = "error while getting secret [%s]: %s".formatted(location.secret(), error.message()); + 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)); } } - // normal case + 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, secretMount); + 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(), secretMount); + 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()); 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 index 05fa4651e..fcc316e6f 100644 --- 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 @@ -27,6 +27,7 @@ public class MockSecretProviderConfiguration implements SecretManagerConfigurati 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; @@ -40,27 +41,45 @@ public MockSecretProviderConfiguration(Map config) { // process errors int i = 0; - String base = error(i); - while (config.containsKey(base + ".secret")) { - String secret = config.get(base + ".secret").toString(); - int repeat = Integer.parseInt(config.getOrDefault(base + ".repeat", String.valueOf(NO_REPEAT)).toString()); - configuredErrors.put(secret, new ConfiguredError(config.getOrDefault(base + ".message", "").toString(), repeat)); - base = error(++i); + 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; - base = event(i); - while (watches.containsKey(base + ".secret")) { + String event = event(i); + while (watches.containsKey(event + ".secret")) { configuredEvents.add( new ConfiguredEvent( - watches.get(base + ".secret").toString(), - SecretEvent.Type.valueOf(watches.getOrDefault(base + ".type", "CREATED").toString()), - ConfigHelper.removePrefix(watches, base + ".data"), - String.valueOf(watches.get(base + ".error")) + 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")) ) ); - base = event(++i); + 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); } } @@ -72,6 +91,14 @@ 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; 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/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 index 82ad64079..7559503ad 100644 --- 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 @@ -39,6 +39,10 @@ void setup() { 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 @@ -49,6 +53,18 @@ void setup() { 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 @@ -67,7 +83,6 @@ void setup() { - secret: apikeys data: {} error: odd enough message to be unique - """ ); final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); @@ -151,4 +166,32 @@ void should_watch_values() { ) .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"))); + } } From 2b1ed06a4bf69ce961cae1bc25a744419bada8ee Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Fri, 11 Oct 2024 09:43:21 +0200 Subject: [PATCH 06/15] refactor: use create key in cache sign. and create cache key from spec --- .../node/api/secrets/runtime/grant/Grant.java | 1 + .../node/api/secrets/runtime/spec/Spec.java | 4 ++-- .../api/secrets/runtime/storage/Cache.java | 15 ++++----------- .../api/secrets/runtime/storage/CacheKey.java | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/CacheKey.java 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 index 9d02b8cc8..d176ff4c1 100644 --- 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 @@ -4,4 +4,5 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ +// TODO use CacheKey public record Grant(String naturalId, String key) {} 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 index 5714e22b1..f1534a646 100644 --- 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 @@ -14,8 +14,8 @@ public record Spec( String id, String name, - String uri, // /vault/secrets/passwords - String key, // 1/ redis, 2/ ldap + String uri, + String key, List children, boolean usesDynamicKey, boolean isOnTheFly, 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 index 305694b39..614481ec7 100644 --- 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 @@ -8,18 +8,11 @@ * @author GraviteeSource Team */ public interface Cache { - CacheKey put(String envId, String naturalId, Entry value); + CacheKey put(CacheKey cacheKey, Entry value); - Optional get(String envId, String naturalId); + Optional get(CacheKey cacheKey); - void computeIfAbsent(String envId, String naturalId, Supplier supplier); + void computeIfAbsent(CacheKey cacheKey, Supplier supplier); - void evict(String envId, String naturalId); - - record CacheKey(String envId, String naturalId) { - @Override - public String toString() { - return envId + "-" + naturalId; - } - } + 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..9fd16bb62 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/storage/CacheKey.java @@ -0,0 +1,18 @@ +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 naturalId) { + @Override + public String toString() { + return envId + "-" + naturalId; + } + + public static CacheKey from(Spec spec) { + return new CacheKey(spec.envId(), spec.naturalId()); + } +} From 90e059e28b550bc86d109693a494cf89256b754f Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Fri, 11 Oct 2024 09:43:29 +0200 Subject: [PATCH 07/15] refactor: use create key in cache sign. and create cache key from spec --- .../services/runtimesecrets/el/Service.java | 6 +- .../renewal/RenewalService.java | 3 +- .../spec/DefaultSpecLifecycleService.java | 10 +-- .../storage/SimpleOffHeapCache.java | 16 ++-- .../runtimesecrets/ProcessorTest.java | 89 ++++++++++--------- .../runtimesecrets/el/ServiceTest.java | 5 +- .../spec/DefaultSpecLifecycleServiceTest.java | 13 +-- .../storage/SimpleOffHeapCacheTest.java | 47 +++++----- 8 files changed, 100 insertions(+), 89 deletions(-) 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 index fe9ceb1d7..ce412e8cc 100644 --- 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 @@ -29,6 +29,7 @@ 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; @@ -46,6 +47,7 @@ public class Service { private final SpecLifecycleService specLifecycleService; private final SpecRegistry specRegistry; + // TODO remove envId public String fromGrant(String contextId, String envId) { Optional grantOptional = grantService.getGrant(contextId); if (grantOptional.isEmpty()) { @@ -66,7 +68,7 @@ private String getFromCache(String envId, Grant grant, String key) { return resultToValue( toResult( cache - .get(envId, grant.naturalId()) + .get(new CacheKey(envId, grant.naturalId())) .orElse( new Entry( Entry.Type.EMPTY, @@ -118,7 +120,7 @@ private String grantAndGet(String envId, String definitionKind, String definitio return resultToValue( toResult( cache - .get(envId, naturalId) + .get(new CacheKey(envId, naturalId)) .orElse( new Entry(Entry.Type.EMPTY, null, "no value in cache for [%s] in environment [%s]".formatted(naturalId, envId)) ), 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 index 0300a1d47..6a95a42fc 100644 --- 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 @@ -21,6 +21,7 @@ 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.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.disposables.Disposable; import java.time.Instant; @@ -99,7 +100,7 @@ private void doStart() { .doOnSuccess(entry -> { // todo update partial setupNextCheck(spec); - cache.put(spec.envId(), spec.naturalId(), entry); + cache.put(CacheKey.from(spec), entry); }); }) .subscribe(); 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 index 93a035be2..aeb9e3a20 100644 --- 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 @@ -26,6 +26,7 @@ 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.annotations.NonNull; import io.reactivex.rxjava3.functions.Action; @@ -61,8 +62,7 @@ public Spec deployOnTheFly(String envId, Ref ref) { Spec runtimeSpec = ref.asOnTheFlySpec(envId); specRegistry.register(runtimeSpec); cache.computeIfAbsent( - envId, - runtimeSpec.naturalId(), + CacheKey.from(runtimeSpec), () -> { SecretURL secretURL = runtimeSpec.toSecretURL(); return resolverService @@ -99,7 +99,7 @@ public void deploy(Spec spec) { renewGrant(update); specRegistry.replace(update); if (!currentSpec.naturalId().equals(spec.naturalId())) { - cache.evict(currentSpec.envId(), currentSpec.naturalId()); + cache.evict(CacheKey.from(currentSpec)); } }; } else if (isACLsChange(update)) { @@ -145,7 +145,7 @@ record LiteSpec(String name, String uriAndKey) {} public void undeploy(Spec spec) { contextRegistry.findBySpec(spec).forEach(grantService::revoke); specRegistry.unregister(spec); - cache.evict(spec.envId(), spec.naturalId()); + cache.evict(CacheKey.from(spec)); renewalService.onDelete(spec); } @@ -159,6 +159,6 @@ private void asyncResolution(Spec spec, Duration delay, @NonNull Action postReso .flatMap(mount -> resolverService.resolve(envId, mount)) .doOnError(err -> log.error("Async resolution failed", err)) .doFinally(postResolution) - .subscribe(entry -> cache.put(spec.envId(), spec.naturalId(), entry)); + .subscribe(entry -> cache.put(CacheKey.from(spec), entry)); } } 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 index be36ebe20..ae6b5b807 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -50,8 +51,7 @@ public SimpleOffHeapCache() { private final ConcurrentMap data = new ConcurrentHashMap<>(); @Override - public CacheKey put(String envId, String naturalId, Entry value) { - final CacheKey cacheKey = new CacheKey(envId, naturalId); + public CacheKey put(CacheKey cacheKey, Entry value) { var bytes = serialize(value); final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length); data.put(cacheKey, byteBuffer); @@ -60,8 +60,8 @@ public CacheKey put(String envId, String naturalId, Entry value) { } @Override - public Optional get(String envId, String naturalId) { - ByteBuffer byteBuffer = data.get(new CacheKey(envId, naturalId)); + public Optional get(CacheKey cacheKey) { + ByteBuffer byteBuffer = data.get(cacheKey); if (byteBuffer != null) { byte[] buf = new byte[byteBuffer.limit()]; byteBuffer.position(0); @@ -72,9 +72,9 @@ public Optional get(String envId, String naturalId) { } @Override - public void computeIfAbsent(String envId, String naturalId, Supplier supplier) { + public void computeIfAbsent(CacheKey cacheKey, Supplier supplier) { data.computeIfAbsent( - new CacheKey(envId, naturalId), + cacheKey, key -> { Entry value = supplier.get(); byte[] stringAsBytes = serialize(value); @@ -86,8 +86,8 @@ public void computeIfAbsent(String envId, String naturalId, Supplier supp } @Override - public void evict(String envId, String naturalId) { - data.remove(new CacheKey(envId, naturalId)); + public void evict(CacheKey cacheKey) { + data.remove(cacheKey); } public byte[] serialize(Entry value) { 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 index 111f682dc..c73ef7a6f 100644 --- 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 @@ -46,6 +46,7 @@ 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; @@ -187,7 +188,7 @@ 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(FOO_ENV_ID, name)).isPresent()); + 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")); @@ -203,8 +204,8 @@ void should_get_a_different_secret_in_two_different_env() { specLifeCycleService.deploy(fooSpec); specLifeCycleService.deploy(barSpec); - awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).isPresent()); - awaitShortly().untilAsserted(() -> assertThat(cache.get(BAR_ENV_ID, name)).isPresent()); + 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")); @@ -239,7 +240,7 @@ void should_discover_deny_access_to_second_secrets_due_to_ACLs() { FOO_ENV_ID ); specLifeCycleService.deploy(spec); - awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, "/mock/mySecret")).isPresent()); + awaitShortly().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent()); FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>"); cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1")); @@ -254,7 +255,7 @@ 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(FOO_ENV_ID, name)).isPresent()); + 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")); @@ -270,7 +271,11 @@ void should_fail_to_resolve_secret_on_the_fly_then_succeeds_after_secret_it_is_p FakeDefinition fakeDefinition = new FakeDefinition("123", "<< /mock/flaky:password>>", null); cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition, Map.of("revision", "1")); - assertThat(cache.get(FOO_ENV_ID, "/mock/flaky")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.ERROR); + 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!!!"); @@ -301,7 +306,7 @@ void should_get_a_different_secret_after_spec_update() { FOO_ENV_ID ); specLifeCycleService.deploy(spec); - awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).isPresent()); + 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")); @@ -341,7 +346,7 @@ void should_fail_getting_secret_when_acls_changes() { FOO_ENV_ID ); specLifeCycleService.deploy(spec); - awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).isPresent()); + 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")); @@ -394,7 +399,7 @@ void should_fail_to_get_secret_after_undeploy() { FOO_ENV_ID ); specLifeCycleService.deploy(spec); - awaitShortly().untilAsserted(() -> assertThat(cache.get(FOO_ENV_ID, name)).isPresent()); + 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")); @@ -417,7 +422,7 @@ void should_go_from_on_the_fly_to_named_user_flow() { // create spec to limit sage String specID = UUID.randomUUID().toString(); - Spec spec = new Spec( + Spec specWithUri = new Spec( specID, null, "/mock/mySecret", @@ -429,7 +434,7 @@ void should_go_from_on_the_fly_to_named_user_flow() { new ACLs(null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); - specLifeCycleService.deploy(spec); + specLifeCycleService.deploy(specWithUri); awaitShortly() .untilAsserted(() -> { assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("fighters"); @@ -441,24 +446,23 @@ void should_go_from_on_the_fly_to_named_user_flow() { // This would let the previous on the fly untouched and not evicted, then when undeployed => remove and evict // create spec to limit sage - spec = - new Spec( - specID, - "redis-password", - "/mock/mySecret", - "redisPassword", - null, - false, - false, - null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), - FOO_ENV_ID - ); - specLifeCycleService.deploy(spec); + Spec specWithName = new Spec( + specID, + "redis-password", + "/mock/mySecret", + "redisPassword", + null, + false, + false, + null, + new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + FOO_ENV_ID + ); + specLifeCycleService.deploy(specWithName); awaitShortly() .untilAsserted(() -> - assertThat(cache.get(FOO_ENV_ID, "redis-password")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE) + assertThat(cache.get(CacheKey.from(specWithName))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE) ); FakeDefinition fakeDefinition2 = new FakeDefinition("123", "<>", "<< redis-password>>"); @@ -466,7 +470,7 @@ void should_go_from_on_the_fly_to_named_user_flow() { awaitShortly() .untilAsserted(() -> { - assertThat(cache.get(FOO_ENV_ID, "redis-password")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); + 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); @@ -491,32 +495,31 @@ void should_evict_unused_secrets_on_the_fly() { cut.onDefinitionUnDeploy(FOO_ENV_ID, fakeDefinitionRev1, Map.of("revision", "1")); assertThat(spelTemplateEngine.getValue(fakeDefinitionRev2.getFirst(), String.class)).isEqualTo("fighters"); - assertThat(cache.get(FOO_ENV_ID, "/mock/secondSecret")).isNotPresent(); + assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/secondSecret"))).isNotPresent(); } @Test void should_renew_secrets() { renewalService.start(); - specLifeCycleService.deploy( - new Spec( - "123456", - "rotating", - "/mock/rotating", - "password", - null, - false, - false, - new Resolution(Resolution.Type.POLL, Duration.ofSeconds(1)), - null, - FOO_ENV_ID - ) + 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); awaitShortly() .atMost(1, TimeUnit.SECONDS) .untilAsserted(() -> { - assertThat(cache.get(FOO_ENV_ID, "rotating")).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); + assertThat(cache.get(CacheKey.from(spec))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); }); FakeDefinition fakeDefinition = new FakeDefinition("123", "<>", "<>"); @@ -524,7 +527,7 @@ void should_renew_secrets() { awaitShortly() .atMost(1, TimeUnit.SECONDS) .untilAsserted(() -> { - assertThat(cache.get(FOO_ENV_ID, "/mock/secondSecret")) + assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/secondSecret"))) .isPresent() .get() .extracting(Entry::type) 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 index 1515a71c8..0b7f338b2 100644 --- 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 @@ -42,6 +42,7 @@ 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; @@ -140,7 +141,7 @@ void should_call_service_using_fromGrant( ) { Spec spec = new Spec(null, specName, "/mock/mySecret", key, null, dynKeys, false, null, null, ENV_ID); specLifeCycleService.deploy(spec); - shortAwait().untilAsserted(() -> assertThat(cache.get(ENV_ID, naturalId)).isPresent()); + shortAwait().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent()); DiscoveryContext context = new DiscoveryContext( UUID.randomUUID(), @@ -176,7 +177,7 @@ void should_call_service_using_fromELWith(String test, String specName, String n 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(ENV_ID, naturalId)).isPresent()); + shortAwait().untilAsserted(() -> assertThat(cache.get(CacheKey.from(spec))).isPresent()); boolean authorized = grantService.grant(context, spec); assertThat(authorized).isTrue(); grantService.grant(context, spec); 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 index 62afe814f..6f216ec3e 100644 --- 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 @@ -32,6 +32,7 @@ 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; @@ -101,7 +102,7 @@ void before() { 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("redis-password")); + Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec)); } @Test @@ -111,7 +112,7 @@ void should_deploy_spec_on_the_fly_then_get_secret_map() { Spec spec = cut.deployOnTheFly(ENV_ID, ref); assertThat(spec.uri()).isEqualTo("/mock/mySecret"); assertThat(spec.key()).isEqualTo("redisPassword"); - Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache("/mock/mySecret")); + Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec)); } @ParameterizedTest @@ -124,13 +125,13 @@ void should_not_deploy_on_the_fly(String s) { 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("redis-password")); + Awaitility.await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> checkInCache(spec)); cut.undeploy(spec); - assertThat(cache.get(ENV_ID, "redis-password")).isNotPresent(); + assertThat(cache.get(CacheKey.from(spec))).isNotPresent(); } - private void checkInCache(String natualId) { - Optional foo = cache.get(ENV_ID, natualId); + private void checkInCache(Spec spec) { + Optional foo = cache.get(CacheKey.from(spec)); assertThat(foo).get().extracting(Entry::type).asString().isEqualTo("VALUE"); assertThat(foo) .get() 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 index d440f0aff..d0636e58e 100644 --- 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 @@ -19,6 +19,7 @@ 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; @@ -38,13 +39,16 @@ class SimpleOffHeapCacheTest { @Test void should_store_error_entries() { - cut.put("dev", "secret_error", new Entry(Entry.Type.ERROR, null, "500")); - cut.put("test", "secret_empty", new Entry(Entry.Type.EMPTY, null, "204")); - cut.put("prod", "secret_not_found", new Entry(Entry.Type.NOT_FOUND, null, "404")); + 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("dev", "secret_error")).get().extracting("type", "error").containsExactly(Entry.Type.ERROR, "500"); - assertThat(cut.get("test", "secret_empty")).get().extracting("type", "error").containsExactly(Entry.Type.EMPTY, "204"); - assertThat(cut.get("prod", "secret_not_found")) + 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") @@ -53,13 +57,13 @@ void should_store_error_entries() { @Test void should_store_segmented_data() { - cut.put("dev", "secret", new Entry(Entry.Type.VALUE, Map.of("foo", new Secret("bar")), null)); - cut.put("test", "secret", new Entry(Entry.Type.VALUE, Map.of("buz", new Secret("puk")), null)); - assertThat(cut.get("dev", "secret")) + 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("test", "secret")) + assertThat(cut.get(new CacheKey("test", "secret"))) .get() .usingRecursiveAssertion() .isEqualTo(new Entry(Entry.Type.VALUE, Map.of("buz", new Secret("puk")), null)); @@ -68,16 +72,15 @@ void should_store_segmented_data() { @Test void should_perform_crud_ops() { cut.put( - "dev", - "secret", + 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("dev", "secret")) + 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("dev", "secret")) + assertThat(cut.get(new CacheKey("dev", "secret"))) .get() .extracting(entry -> entry.value().keySet()) .asInstanceOf(InstanceOfAssertFactories.COLLECTION) @@ -89,19 +92,19 @@ void should_perform_crud_ops() { Map.of("mongodb-password", new Secret("778899"), "mysql-password", new Secret("qwerty")), null ); - cut.put("dev", "secret", dbPasswords); - dbPasswordsAssert(cut.get("dev", "secret")); + cut.put(new CacheKey("dev", "secret"), dbPasswords); + dbPasswordsAssert(cut.get(new CacheKey("dev", "secret"))); // no override as does not exists - cut.computeIfAbsent("dev", "secret", () -> new Entry(Entry.Type.VALUE, Map.of(), null)); - dbPasswordsAssert(cut.get("dev", "secret")); + cut.computeIfAbsent(new CacheKey("dev", "secret"), () -> new Entry(Entry.Type.VALUE, Map.of(), null)); + dbPasswordsAssert(cut.get(new CacheKey("dev", "secret"))); // eviction - cut.evict("dev", "secret"); - assertThat(cut.get("dev", "secret")).isNotPresent(); + cut.evict(new CacheKey("dev", "secret")); + assertThat(cut.get(new CacheKey("dev", "secret"))).isNotPresent(); - cut.computeIfAbsent("dev", "secret", () -> dbPasswords); - dbPasswordsAssert(cut.get("dev", "secret")); + cut.computeIfAbsent(new CacheKey("dev", "secret"), () -> dbPasswords); + dbPasswordsAssert(cut.get(new CacheKey("dev", "secret"))); } private void dbPasswordsAssert(Optional optEntry) { From 9010dd17bb62200f3bebb5fafaf1d733e3f5f09c Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Mon, 14 Oct 2024 09:06:51 +0200 Subject: [PATCH 08/15] refactor: use cache key in grant and simplify EL --- .../node/api/secrets/runtime/grant/Grant.java | 6 ++++-- .../services/runtimesecrets/el/Formatter.java | 18 +++++++++--------- .../services/runtimesecrets/el/Service.java | 18 ++++++++++-------- .../grant/DefaultGrantService.java | 3 ++- .../runtimesecrets/el/ServiceTest.java | 1 - 5 files changed, 25 insertions(+), 21 deletions(-) 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 index d176ff4c1..a6c0cadec 100644 --- 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 @@ -1,8 +1,10 @@ package io.gravitee.node.api.secrets.runtime.grant; +import io.gravitee.node.api.secrets.runtime.storage.CacheKey; + /** * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -// TODO use CacheKey -public record Grant(String naturalId, String key) {} + +public record Grant(CacheKey cacheKey, String secretKey) {} 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 index a5473f64b..9726529d8 100644 --- 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 @@ -27,8 +27,8 @@ */ public class Formatter { - public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s', '%s')}"; - public static final String FROM_GRANT_EL_KEY_TEMPLATE = "{#secrets.fromGrant('%s', '%s', %s)}"; + public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s')}"; + public static final String FROM_GRANT_EL_KEY_TEMPLATE = "{#secrets.fromGrant('%s', %s)}"; 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)}"; @@ -44,9 +44,9 @@ public static String computeELFromStatic(DiscoveryContext context, String envId) switch (context.ref().secondaryType()) { case KEY -> { if (context.ref().secondaryExpression().isLiteral()) { - el = fromGrant(context.id(), envId); + el = fromGrant(context.id()); } else { - el = fromGrant(context.id(), envId, context.ref().secondaryExpression().value()); + el = fromGrant(context.id(), context.ref().secondaryExpression().value()); } } case NAME -> el = @@ -58,7 +58,7 @@ public static String computeELFromStatic(DiscoveryContext context, String envId) } } } else { - el = fromGrant(context.id(), envId); + el = fromGrant(context.id()); } return el; } @@ -108,12 +108,12 @@ private static String fromGrantWithTemplate( return FROM_GRANT_WITH_TEMPLATE.formatted(methodSuffix, id, envId, literalExpression, quoteLiteral(secondaryExpression)); } - private static String fromGrant(UUID id, String envId) { - return FROM_GRANT_TEMPLATE.formatted(id, envId); + private static String fromGrant(UUID id) { + return FROM_GRANT_TEMPLATE.formatted(id); } - private static String fromGrant(UUID id, String envId, String key) { - return FROM_GRANT_EL_KEY_TEMPLATE.formatted(id, envId, key); + private static String fromGrant(UUID id, String key) { + return FROM_GRANT_EL_KEY_TEMPLATE.formatted(id, key); } private static String quoteLiteral(Ref.Expression expression) { 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 index ce412e8cc..5d7447878 100644 --- 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 @@ -47,33 +47,35 @@ public class Service { private final SpecLifecycleService specLifecycleService; private final SpecRegistry specRegistry; - // TODO remove envId - public String fromGrant(String contextId, String envId) { + public String fromGrant(String contextId) { Optional grantOptional = grantService.getGrant(contextId); if (grantOptional.isEmpty()) { return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); } - return getFromCache(envId, grantOptional.get(), grantOptional.get().key()); + return getFromCache(grantOptional.get(), grantOptional.get().secretKey()); } - public String fromGrant(String contextId, String envId, String key) { + public String fromGrant(String contextId, String key) { Optional grantOptional = grantService.getGrant(contextId); if (grantOptional.isEmpty()) { return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); } - return getFromCache(envId, grantOptional.get(), key); + return getFromCache(grantOptional.get(), key); } - private String getFromCache(String envId, Grant grant, String key) { + private String getFromCache(Grant grant, String key) { return resultToValue( toResult( cache - .get(new CacheKey(envId, grant.naturalId())) + .get(grant.cacheKey()) .orElse( new Entry( Entry.Type.EMPTY, null, - "no value in cache for [%s] in environment [%s]".formatted(grant.naturalId(), envId) + "no value in cache for [%s] in environment [%s]".formatted( + grant.cacheKey().naturalId(), + grant.cacheKey().envId() + ) ) ), key 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 index bf1b9dd5b..26a0236f4 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -50,7 +51,7 @@ public class DefaultGrantService implements GrantService { 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(spec.naturalId(), spec.key())); + grantRegistry.register(context.id().toString(), new Grant(CacheKey.from(spec), spec.key())); } return granted; } 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 index 0b7f338b2..77ecc33a6 100644 --- 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 @@ -151,7 +151,6 @@ void should_call_service_using_fromGrant( ); boolean authorized = grantService.grant(context, spec); assertThat(authorized).isTrue(); - grantService.grant(context, spec); String el = Formatter.computeELFromStatic(context, ENV_ID); assertThat(spelTemplateEngine.getValue(el, String.class)).isEqualTo("redisadmin"); From 0c8f1f3f8ff611fded32fbb2319a7d833def8658 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Mon, 14 Oct 2024 21:17:52 +0200 Subject: [PATCH 09/15] feat: allow partial renewal, cache key is base on uri. --- .../api/secrets/runtime/storage/Cache.java | 6 ++- .../api/secrets/runtime/storage/CacheKey.java | 11 ++-- .../services/runtimesecrets/Processor.java | 4 +- .../runtimesecrets/RuntimeSecretsService.java | 43 +++++++-------- .../discovery/DefaultContextRegistry.java | 49 ++++++++++++----- .../discovery/PayloadRefParser.java | 6 +-- .../runtimesecrets/discovery/RefParser.java | 4 +- .../services/runtimesecrets/el/Service.java | 33 +++++------- .../providers/SecretProviderRegistry.java | 37 ++++++++----- .../renewal/RenewalService.java | 54 ++++++++++++------- .../spec/DefaultSpecLifecycleService.java | 14 ++--- .../runtimesecrets/spec/SpecRegistry.java | 15 ++---- .../storage/SimpleOffHeapCache.java | 23 +++++++- .../runtimesecrets/ProcessorTest.java | 22 ++++---- 14 files changed, 190 insertions(+), 131 deletions(-) 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 index 614481ec7..d4ab9bf14 100644 --- 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 @@ -1,5 +1,7 @@ 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; @@ -8,7 +10,9 @@ * @author GraviteeSource Team */ public interface Cache { - CacheKey put(CacheKey cacheKey, Entry value); + void put(CacheKey cacheKey, Entry value); + + void putPartial(CacheKey cacheKey, Map partial); Optional get(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 index 9fd16bb62..c4499ab21 100644 --- 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 @@ -6,13 +6,18 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record CacheKey(String envId, String naturalId) { +public record CacheKey(String envId, String uri) implements Comparable { @Override public String toString() { - return envId + "-" + naturalId; + return envId + "-" + uri; } public static CacheKey from(Spec spec) { - return new CacheKey(spec.envId(), spec.naturalId()); + return new CacheKey(spec.envId(), spec.uri()); + } + + @Override + public int compareTo(CacheKey o) { + return this.toString().compareTo(o.toString()); } } 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 index 702bd1a46..6306f1f1d 100644 --- 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 @@ -52,7 +52,7 @@ public class Processor { *

  • Inject EL {@link PayloadRefParser}
  • *
  • Find {@link Spec} or create on the fly
  • *
  • Grant {@link DiscoveryContext}
  • - * @param definition the secret naturalId container + * @param definition the secret ref container * @param metadata some optional metadata * @param the kind of subject */ @@ -70,7 +70,7 @@ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl DefaultPayloadNotifier notifier = new DefaultPayloadNotifier(rootDefinition, envId, specRegistry); definitionBrowser.findPayloads(definition, notifier); - // register contexts by naturalId and definition + // register contexts by ref and definition for (DiscoveryContext context : notifier.getContextList()) { contextRegistry.register(context, rootDefinition); // get spec 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 index d5ef04cc7..09351d9bc 100644 --- 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 @@ -59,6 +59,22 @@ protected void doStart() throws Exception { 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( @@ -91,22 +107,6 @@ private void 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 startWatch(String directory) { if (directory == null) { return; @@ -157,20 +157,21 @@ private void startWatch(String directory) { private void handleDemo(Properties properties) { String pluginToAdd = properties.getProperty("updateSpecACLAddPlugin", "ignore"); String specToUndeploy = properties.getProperty("undeploySpec", ""); - String addSpec = properties.getProperty("newSpecWithACL", ""); + String otfSpecWithACL = properties.getProperty("otfSpecWithACL", ""); + int otfSpecWithRenewal = Integer.parseInt(properties.getProperty("otfSpecWithRenewal", "0")); - if (!addSpec.isEmpty()) { + if (!otfSpecWithACL.isEmpty()) { specLifecycleService.deploy( new Spec( "f9024ec8-ad20-4834-8962-9c9153218983", - null, + "case-1-api-key", "/mock/static/uri", "api-key", null, false, false, - null, - acls(addSpec), + otfSpecWithRenewal > 0 ? new Resolution(Resolution.Type.POLL, Duration.ofSeconds(otfSpecWithRenewal)) : null, + acls(otfSpecWithACL), "DEFAULT" ) ); 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 index 2cb907262..4b8f81387 100644 --- 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 @@ -65,47 +65,72 @@ static class InternalRegistry implements ContextRegistry { public void register(DiscoveryContext context, Definition definition) { if (context.ref().mainType() == Ref.MainType.NAME && context.ref().mainExpression().isLiteral()) { - byName.put(context.ref().mainExpression().value(), context); + synchronized (byName) { + byName.put(context.ref().mainExpression().value(), context); + } } if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { - byUri.put(context.ref().mainExpression().value(), context); + synchronized (byUri) { + byUri.put(context.ref().mainExpression().value(), context); + } + if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { - byUriAndKey.put(context.ref().uriAndKey(), context); + synchronized (byUriAndKey) { + byUriAndKey.put(context.ref().uriAndKey(), context); + } } } - byDefinitionSpec.put(definition, context); + synchronized (byDefinitionSpec) { + byDefinitionSpec.put(definition, context); + } } public List findBySpec(Spec spec) { List result = new ArrayList<>(); if (spec.name() != null && !spec.name().isEmpty()) { - result.addAll(byName.get(spec.name())); + synchronized (byName) { + result.addAll(byName.get(spec.name())); + } } if (spec.uri() != null && !spec.uri().isEmpty()) { - result.addAll(byUri.get(spec.name())); + synchronized (byUri) { + result.addAll(byUri.get(spec.name())); + } } if (spec.key() != null && !spec.key().isEmpty()) { - result.addAll(byUriAndKey.get(spec.uriAndKey())); + synchronized (byUriAndKey) { + result.addAll(byUriAndKey.get(spec.uriAndKey())); + } } return result; } public List getByDefinition(String envId, Definition definition) { - return List.copyOf(byDefinitionSpec.get(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()) { - byName.remove(context.ref().mainExpression().value(), context); + synchronized (byName) { + byName.remove(context.ref().mainExpression().value(), context); + } } if (context.ref().mainType() == Ref.MainType.URI && context.ref().mainExpression().isLiteral()) { - byUri.remove(context.ref().mainExpression().value(), context); + synchronized (byUri) { + byUri.remove(context.ref().mainExpression().value(), context); + } if (context.ref().secondaryType() == Ref.SecondaryType.KEY && context.ref().secondaryExpression().isLiteral()) { - byUriAndKey.remove(context.ref().uriAndKey(), context); + synchronized (byUriAndKey) { + byUriAndKey.remove(context.ref().uriAndKey(), context); + } } } - byDefinitionSpec.put(definition, 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/PayloadRefParser.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/discovery/PayloadRefParser.java index 985f1d4f4..7f6bcebf9 100644 --- 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 @@ -70,13 +70,13 @@ public List runDiscovery() { public String replaceRefs(List expressions) { if (expressions.size() != this.rawRefs.size()) { - throw new IllegalArgumentException("naturalId and replacement list don't match in 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 naturalId by expression + // replace ref by expression Position position = rawRefs.get(i).position; payload.replace(position.start, position.end, replacement); @@ -84,7 +84,7 @@ public String replaceRefs(List expressions) { int refStringLength = position.end - position.start; int replacementLength = replacement.length(); int lengthDiff = replacementLength - refStringLength; - // apply offset change on next naturalId positions + // apply offset change on next ref positions for (int p = i + 1; p < expressions.size(); p++) { rawRefs.get(p).position.move(lengthDiff); } 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 index 0169b91d2..c4dc9fff9 100644 --- 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 @@ -53,13 +53,13 @@ public class RefParser { * 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 naturalId (with start and end separator + * @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("naturalId is null or empty"); + throw new SecretRefParsingException("ref is null or empty"); } var buffer = new StringBuilder(ref); // delete 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 index 5d7447878..600f23b2e 100644 --- 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 @@ -72,10 +72,7 @@ private String getFromCache(Grant grant, String key) { new Entry( Entry.Type.EMPTY, null, - "no value in cache for [%s] in environment [%s]".formatted( - grant.cacheKey().naturalId(), - grant.cacheKey().envId() - ) + "no value in cache for [%s] in environment [%s]".formatted(grant.cacheKey().uri(), grant.cacheKey().envId()) ) ), key @@ -99,7 +96,7 @@ public String fromELWithUri(String envId, String uriWithKey, String definitionKi if (spec == null && specLifecycleService.shouldDeployOnTheFly(ref)) { spec = specLifecycleService.deployOnTheFly(envId, ref); } - return grantAndGet(envId, definitionKind, definitionId, spec, ref, uriAndKey.uri(), uriAndKey.key()); + 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")); } @@ -108,23 +105,27 @@ public String fromELWithUri(String envId, String uriWithKey, String definitionKi 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, name, spec.key()); + return grantAndGet(envId, definitionKind, definitionId, spec, ref, spec.key()); } - private String grantAndGet(String envId, String definitionKind, String definitionId, Spec spec, Ref ref, String naturalId, String 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(naturalId, envId))); + resultToValue(new Result(Result.Type.DENIED, "secret [%s] is denied in environment [%s]".formatted(spec.naturalId(), envId))); } return resultToValue( toResult( cache - .get(new CacheKey(envId, naturalId)) + .get(CacheKey.from(spec)) .orElse( - new Entry(Entry.Type.EMPTY, null, "no value in cache for [%s] in environment [%s]".formatted(naturalId, envId)) + new Entry( + Entry.Type.EMPTY, + null, + "no value in cache for [%s] in environment [%s]".formatted(spec.naturalId(), envId) + ) ), key ) @@ -143,15 +144,9 @@ private Result toResult(Entry entry, String key) { 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()); - } + 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; 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 index 5f1c14ac9..5519ec2a9 100644 --- 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 @@ -22,9 +22,10 @@ import io.gravitee.node.secrets.service.AbstractSecretProviderDispatcher; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; -import java.util.HashMap; +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) @@ -32,14 +33,16 @@ */ public class SecretProviderRegistry { - Multimap perEnv = MultimapBuilder.hashKeys().arrayListValues().build(); - Map allEnvs = new HashMap<>(); + 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 { - perEnv.put(envId, new SecretProviderEntry(id, provider)); + synchronized (perEnv) { + perEnv.put(envId, new SecretProviderEntry(id, provider)); + } } } @@ -52,17 +55,23 @@ public void register(String id, SecretProvider provider, String envId) { */ public Single get(String envId, String id) { return Maybe - .fromOptional( - perEnv - .get(envId) - .stream() - .filter(entry -> entry.id().equals(id)) - .map(SecretProviderEntry::provider) - .findFirst() - .or(() -> Optional.ofNullable(allEnvs.get(id))) - ) + .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())); } - public record SecretProviderEntry(String id, SecretProvider provider) {} + record SecretProviderEntry(String id, SecretProvider provider) {} } 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 index 6a95a42fc..4df3be4b3 100644 --- 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 @@ -16,15 +16,21 @@ 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; @@ -50,14 +56,12 @@ public void onSpec(Spec spec) { } public void onSpec(SpecUpdate specUpdate) { + Spec oldSpec = specUpdate.oldSpec(); + if (oldSpec != null) { + specsToRenew.remove(oldSpec); + } if (specUpdate.newSpec().hasResolutionType(Resolution.Type.POLL)) { - synchronized (specsToRenew) { - Spec oldSpec = specUpdate.oldSpec(); - if (oldSpec != null) { - specsToRenew.remove(oldSpec); - } - setupNextCheck(specUpdate.newSpec()); - } + setupNextCheck(specUpdate.newSpec()); } } @@ -90,19 +94,31 @@ private void doStart() { .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)); + }) ) - // todo group-by - .concatMapSingle(spec -> { - log.info("Renewing secret for spec {}", spec); - return resolverService - .toSecretMount(spec.envId(), spec.toSecretURL()) - .flatMap(secretMount -> resolverService.resolve(spec.envId(), secretMount)) - .doOnSuccess(entry -> { - // todo update partial - setupNextCheck(spec); - cache.put(CacheKey.from(spec), entry); - }); - }) .subscribe(); } 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 index aeb9e3a20..cc3f97d65 100644 --- 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 @@ -93,13 +93,13 @@ public void deploy(Spec spec) { boolean shouldResolve = true; if (currentSpec != null) { SpecUpdate update = new SpecUpdate(currentSpec, spec); - if (isNameOrLocationChanged(update)) { + if (isUriAndKeyChanged(update)) { afterResolve = () -> { renewGrant(update); specRegistry.replace(update); - if (!currentSpec.naturalId().equals(spec.naturalId())) { - cache.evict(CacheKey.from(currentSpec)); + if (!Objects.equals(CacheKey.from(update.oldSpec()), CacheKey.from(update.newSpec()))) { + cache.evict(CacheKey.from(update.oldSpec())); } }; } else if (isACLsChange(update)) { @@ -133,12 +133,8 @@ private static boolean isACLsChange(SpecUpdate update) { return !Objects.equals(update.oldSpec().acls(), update.newSpec().acls()); } - private boolean isNameOrLocationChanged(SpecUpdate update) { - record LiteSpec(String name, String uriAndKey) {} - return !Objects.equals( - new LiteSpec(update.oldSpec().name(), update.oldSpec().uriAndKey()), - new LiteSpec(update.newSpec().name(), update.newSpec().uriAndKey()) - ); + private boolean isUriAndKeyChanged(SpecUpdate update) { + return !Objects.equals(update.oldSpec().uriAndKey(), update.newSpec().uriAndKey()); } @Override 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 index 3782d7fec..8851b1211 100644 --- 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 @@ -17,7 +17,6 @@ import io.gravitee.node.api.secrets.runtime.discovery.Ref; import io.gravitee.node.api.secrets.runtime.spec.Spec; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; @@ -63,19 +62,15 @@ public Spec fromRef(String envId, Ref query) { return registry(envId).fromRef(query); } - public Spec fromId(String envId, String id) { - return registry(envId).fromId(id); - } - private Registry registry(String envId) { return registries.computeIfAbsent(envId, ignore -> new Registry()); } private static class Registry { - private final Map byName = new HashMap<>(); - private final Map byUriAndKey = new HashMap<>(); - private final Map byID = new HashMap<>(); + 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) { @@ -134,9 +129,5 @@ Spec fromSpec(Spec query) { } return result; } - - Spec fromId(String id) { - return byID.get(id); - } } } 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 index ae6b5b807..2cbe31555 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -51,12 +52,30 @@ public SimpleOffHeapCache() { private final ConcurrentMap data = new ConcurrentHashMap<>(); @Override - public CacheKey put(CacheKey cacheKey, Entry value) { + 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); - return cacheKey; + } + + @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 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 index c73ef7a6f..78be53632 100644 --- 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 @@ -442,9 +442,6 @@ void should_go_from_on_the_fly_to_named_user_flow() { .isInstanceOf(SecretAccessDeniedException.class); }); - // TODO, this needs to be refined. The cache key should the Id. CacheKey.from(spec) ? - // This would let the previous on the fly untouched and not evicted, then when undeployed => remove and evict - // create spec to limit sage Spec specWithName = new Spec( specID, @@ -468,7 +465,8 @@ void should_go_from_on_the_fly_to_named_user_flow() { FakeDefinition fakeDefinition2 = new FakeDefinition("123", "<>", "<< redis-password>>"); cut.onDefinitionDeploy(FOO_ENV_ID, fakeDefinition2, Map.of("revision", "2")); - awaitShortly() + 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"); @@ -516,23 +514,23 @@ void should_renew_secrets() { ); specLifeCycleService.deploy(spec); - awaitShortly() + await() .atMost(1, TimeUnit.SECONDS) - .untilAsserted(() -> { - assertThat(cache.get(CacheKey.from(spec))).isPresent().get().extracting(Entry::type).isEqualTo(Entry.Type.VALUE); - }); + .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")); - awaitShortly() + await() .atMost(1, TimeUnit.SECONDS) - .untilAsserted(() -> { + .untilAsserted(() -> assertThat(cache.get(new CacheKey(FOO_ENV_ID, "/mock/secondSecret"))) .isPresent() .get() .extracting(Entry::type) - .isEqualTo(Entry.Type.VALUE); - }); + .isEqualTo(Entry.Type.VALUE) + ); assertThat(spelTemplateEngine.getValue(fakeDefinition.getFirst(), String.class)).isEqualTo("secret1"); assertThat(spelTemplateEngine.getValue(fakeDefinition.getSecond(), String.class)).isEqualTo("yeah!"); From eea78b524453d25a56402d36ecc1ae70561fb1ef Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Fri, 18 Oct 2024 09:10:53 +0200 Subject: [PATCH 10/15] fix: demo --- .../services/runtimesecrets/RuntimeSecretsService.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 09351d9bc..c3ba548ca 100644 --- 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 @@ -165,7 +165,7 @@ private void handleDemo(Properties properties) { new Spec( "f9024ec8-ad20-4834-8962-9c9153218983", "case-1-api-key", - "/mock/static/uri", + "/mock/case1", "api-key", null, false, @@ -182,7 +182,9 @@ private void handleDemo(Properties properties) { if (!specToUndeploy.isEmpty()) { Spec spec = specRegistry.getFromName("DEFAULT", specToUndeploy); - specLifecycleService.undeploy(spec); + if (spec != null) { + specLifecycleService.undeploy(spec); + } } } @@ -190,8 +192,8 @@ private void deployStaticApiKey(String pluginToAdd) { specLifecycleService.deploy( new Spec( "e69328d2-cdb0-4970-a94e-c521ff03f1d5", - "static-api-key", - "/mock/static/named", + "case2-api-key", + "/mock/case2", "api-key", null, false, From a5c6ea202c580378c44128a4e0c4a2ec38939b7b Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Fri, 18 Oct 2024 20:59:46 +0200 Subject: [PATCH 11/15] feat: secret kind and field check --- .../node/api/secrets/runtime/grant/Grant.java | 17 ++++++++++++- .../secrets/runtime/grant/RuntimeContext.java | 11 ++++++++ .../node/api/secrets/runtime/spec/ACLs.java | 2 +- .../node/api/secrets/runtime/spec/Spec.java | 24 ++++++++++++++++++ .../api/secrets/runtime/spec/ValueKind.java | 14 +++++++++++ .../runtimesecrets/RuntimeSecretsService.java | 23 +++++++++++------ .../services/runtimesecrets/el/Formatter.java | 6 +++-- .../services/runtimesecrets/el/Service.java | 15 +++++------ .../grant/DefaultGrantService.java | 5 +++- .../spec/DefaultSpecLifecycleService.java | 9 +++++-- .../runtimesecrets/ProcessorTest.java | 16 ++++++------ .../grant/DefaultGrantServiceTest.java | 25 +++++++++++-------- .../testsupport/SpecFixtures.java | 15 +++++++++-- 13 files changed, 139 insertions(+), 43 deletions(-) create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java create mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java 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 index a6c0cadec..7c55e7aa7 100644 --- 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 @@ -1,10 +1,25 @@ package io.gravitee.node.api.secrets.runtime.grant; +import io.gravitee.node.api.secrets.runtime.spec.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) {} +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/RuntimeContext.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java new file mode 100644 index 000000000..54325c8f9 --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java @@ -0,0 +1,11 @@ +package io.gravitee.node.api.secrets.runtime.grant; + +import io.gravitee.node.api.secrets.runtime.spec.ValueKind; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public record RuntimeContext(boolean allowed, ValueKind allowKind, String field) { + public static final String EL_VARIABLE = "runtime_secrets_context"; +} 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 index 84d777605..0509beb19 100644 --- 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 @@ -7,7 +7,7 @@ * @author GraviteeSource Team */ -public record ACLs(List definitions, List plugins) { +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/Spec.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/Spec.java index f1534a646..6fe9aafaf 100644 --- 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 @@ -6,6 +6,9 @@ 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) @@ -53,6 +56,27 @@ 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) { diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java new file mode 100644 index 000000000..d86ac866e --- /dev/null +++ b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java @@ -0,0 +1,14 @@ +package io.gravitee.node.api.secrets.runtime.spec; + +/** + * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) + * @author GraviteeSource Team + */ +public enum ValueKind { + GENERIC, + PASSWORD, + HEADER, + PRIVATE_KEY, + PUBLIC_KEY, + KEYSTORE, +} 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 index c3ba548ca..ba179091d 100644 --- 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 @@ -19,10 +19,7 @@ import com.graviteesource.services.runtimesecrets.spec.SpecRegistry; 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.gravitee.node.api.secrets.runtime.spec.*; import io.reactivex.rxjava3.schedulers.Schedulers; import java.io.FileReader; import java.io.IOException; @@ -156,6 +153,8 @@ private void startWatch(String directory) { 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")); @@ -177,7 +176,7 @@ private void handleDemo(Properties properties) { ); } if (!pluginToAdd.equals("ignore")) { - deployStaticApiKey(pluginToAdd); + deployStaticApiKey(pluginToAdd, valueKind, fieldToAdd); } if (!specToUndeploy.isEmpty()) { @@ -188,7 +187,7 @@ private void handleDemo(Properties properties) { } } - private void deployStaticApiKey(String pluginToAdd) { + private void deployStaticApiKey(String pluginToAdd, String valueKind, String fieldToAdd) { specLifecycleService.deploy( new Spec( "e69328d2-cdb0-4970-a94e-c521ff03f1d5", @@ -199,16 +198,24 @@ private void deployStaticApiKey(String pluginToAdd) { false, false, null, - acls(pluginToAdd), + 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(null, List.of(new ACLs.PluginACL(pluginToAdd, 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/el/Formatter.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/el/Formatter.java index 9726529d8..5d2111849 100644 --- 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 @@ -15,6 +15,8 @@ */ package com.graviteesource.services.runtimesecrets.el; +import static io.gravitee.node.api.secrets.runtime.grant.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; @@ -27,8 +29,8 @@ */ public class Formatter { - public static final String FROM_GRANT_TEMPLATE = "{#secrets.fromGrant('%s')}"; - public static final String FROM_GRANT_EL_KEY_TEMPLATE = "{#secrets.fromGrant('%s', %s)}"; + 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)}"; 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 index 600f23b2e..b927de057 100644 --- 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 @@ -26,6 +26,7 @@ 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.grant.RuntimeContext; 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; @@ -47,20 +48,20 @@ public class Service { private final SpecLifecycleService specLifecycleService; private final SpecRegistry specRegistry; - public String fromGrant(String contextId) { + public String fromGrant(String contextId, RuntimeContext runtimeContext) { Optional grantOptional = grantService.getGrant(contextId); - if (grantOptional.isEmpty()) { - return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); + 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 key) { + public String fromGrant(String contextId, String secretKey, RuntimeContext runtimeContext) { Optional grantOptional = grantService.getGrant(contextId); - if (grantOptional.isEmpty()) { - return resultToValue(new Result(Result.Type.DENIED, "secret was denied ahead of traffic")); + if (grantOptional.isEmpty() || !grantOptional.get().match(runtimeContext)) { + return resultToValue(new Result(Result.Type.DENIED, "secret is denied")); } - return getFromCache(grantOptional.get(), key); + return getFromCache(grantOptional.get(), secretKey); } private String getFromCache(Grant grant, String key) { 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 index 26a0236f4..e32cc0645 100644 --- 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 @@ -51,7 +51,10 @@ public class DefaultGrantService implements GrantService { 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())); + grantRegistry.register( + context.id().toString(), + new Grant(CacheKey.from(spec), spec.key(), spec.valueKind(), spec.allowedFields()) + ); } return granted; } 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 index cc3f97d65..84eeb3b45 100644 --- 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 @@ -15,6 +15,8 @@ */ 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; @@ -33,6 +35,7 @@ import java.time.Duration; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -129,8 +132,10 @@ private void renewGrant(SpecUpdate update) { }); } - private static boolean isACLsChange(SpecUpdate update) { - return !Objects.equals(update.oldSpec().acls(), update.newSpec().acls()); + 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) { 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 index 78be53632..e72a2d3cb 100644 --- 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 @@ -236,7 +236,7 @@ void should_discover_deny_access_to_second_secrets_due_to_ACLs() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(spec); @@ -302,7 +302,7 @@ void should_get_a_different_secret_after_spec_update() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(spec); @@ -321,7 +321,7 @@ void should_get_a_different_secret_after_spec_update() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(specV2); @@ -342,7 +342,7 @@ void should_fail_getting_secret_when_acls_changes() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(spec); @@ -361,7 +361,7 @@ void should_fail_getting_secret_when_acls_changes() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("second", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("second", null))), FOO_ENV_ID ); specLifeCycleService.deploy(specV2); @@ -395,7 +395,7 @@ void should_fail_to_get_secret_after_undeploy() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(spec); @@ -431,7 +431,7 @@ void should_go_from_on_the_fly_to_named_user_flow() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(specWithUri); @@ -452,7 +452,7 @@ void should_go_from_on_the_fly_to_named_user_flow() { false, false, null, - new ACLs(null, List.of(new ACLs.PluginACL("first", null))), + new ACLs(null, null, List.of(new ACLs.PluginACL("first", null))), FOO_ENV_ID ); specLifeCycleService.deploy(specWithName); 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 index 48fa81375..3da0d8664 100644 --- 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 @@ -56,16 +56,16 @@ 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(List.of(), List.of()), null)), + 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(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), null) + 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(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pwd") + spec("dev", new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pwd") ), arguments( "def acl ok many", @@ -73,6 +73,7 @@ public static Stream grants() { spec( "dev", new ACLs( + null, List.of(new ACLs.DefinitionACL("dict", List.of("123")), new ACLs.DefinitionACL("api", List.of("123", "456"))), null ), @@ -84,7 +85,7 @@ public static Stream grants() { context("dev", "api", "123", plugin("foo")), spec( "dev", - new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null))), + new ACLs(null, List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("foo", null))), null ) ), @@ -94,6 +95,7 @@ public static Stream grants() { 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)) ), @@ -103,7 +105,7 @@ public static Stream grants() { arguments( "plugin acl only", context("dev", "api", "123", plugin("foo")), - spec("dev", new ACLs(null, List.of(new ACLs.PluginACL("foo", null))), null) + spec("dev", new ACLs(null, null, List.of(new ACLs.PluginACL("foo", null))), null) ), arguments( "plugin acl only many", @@ -111,6 +113,7 @@ public static Stream grants() { 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)) ), @@ -127,36 +130,36 @@ public static Stream denials() { arguments( "def acl ok wrong env", context("dev", "api", "123"), - spec("test", new ACLs(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), null) + 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(List.of(new ACLs.DefinitionACL("api", List.of("123"))), null), "pass") + 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(List.of(new ACLs.DefinitionACL("api", List.of("456"))), null), null) + 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(List.of(new ACLs.DefinitionACL("dict", List.of("123"))), null), null) + 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(List.of(new ACLs.DefinitionACL("api", List.of("123"))), List.of(new ACLs.PluginACL("bar", null))), + 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, List.of(new ACLs.PluginACL("foo", null))), null) + spec("dev", new ACLs(null, null, List.of(new ACLs.PluginACL("foo", null))), null) ), arguments("no spec", context("dev", "api", "123", plugin("bar")), 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 index c5159e700..d8677f438 100644 --- 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 @@ -59,12 +59,23 @@ public static Spec fromNameUriAndKeyACLs( false, false, null, - new ACLs(List.of(definitionACL), List.of(pluginACL)), + 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, List.of(pluginACL)), envId); + return new Spec( + UUID.randomUUID().toString(), + name, + uri, + key, + null, + false, + false, + null, + new ACLs(null, null, List.of(pluginACL)), + envId + ); } } From 115dd341ff13a944a3602e244c1e32947fce67cc Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Mon, 21 Oct 2024 18:09:31 +0200 Subject: [PATCH 12/15] fix: move field ACL in common --- .../node/api/secrets/runtime/grant/Grant.java | 3 ++- .../api/secrets/runtime/grant/RuntimeContext.java | 11 ----------- .../node/api/secrets/runtime/spec/ACLs.java | 1 + .../node/api/secrets/runtime/spec/Spec.java | 1 + .../node/api/secrets/runtime/spec/ValueKind.java | 14 -------------- .../runtimesecrets/RuntimeSecretsService.java | 6 +++++- .../services/runtimesecrets/el/Formatter.java | 2 +- .../services/runtimesecrets/el/Service.java | 2 +- pom.xml | 2 +- 9 files changed, 12 insertions(+), 30 deletions(-) delete mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java delete mode 100644 gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java 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 index 7c55e7aa7..7614f76e9 100644 --- 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 @@ -1,6 +1,7 @@ package io.gravitee.node.api.secrets.runtime.grant; -import io.gravitee.node.api.secrets.runtime.spec.ValueKind; +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; diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java deleted file mode 100644 index 54325c8f9..000000000 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/grant/RuntimeContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.gravitee.node.api.secrets.runtime.grant; - -import io.gravitee.node.api.secrets.runtime.spec.ValueKind; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public record RuntimeContext(boolean allowed, ValueKind allowKind, String field) { - public static final String EL_VARIABLE = "runtime_secrets_context"; -} 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 index 0509beb19..8aaed265e 100644 --- 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 @@ -1,5 +1,6 @@ package io.gravitee.node.api.secrets.runtime.spec; +import io.gravitee.common.secrets.ValueKind; import java.util.List; /** 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 index 6fe9aafaf..c27015318 100644 --- 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 @@ -2,6 +2,7 @@ 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; diff --git a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java deleted file mode 100644 index d86ac866e..000000000 --- a/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/ValueKind.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.gravitee.node.api.secrets.runtime.spec; - -/** - * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) - * @author GraviteeSource Team - */ -public enum ValueKind { - GENERIC, - PASSWORD, - HEADER, - PRIVATE_KEY, - PUBLIC_KEY, - KEYSTORE, -} 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 index ba179091d..c45595e02 100644 --- 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 @@ -17,9 +17,13 @@ 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.*; +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; 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 index 5d2111849..29b481702 100644 --- 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 @@ -15,7 +15,7 @@ */ package com.graviteesource.services.runtimesecrets.el; -import static io.gravitee.node.api.secrets.runtime.grant.RuntimeContext.EL_VARIABLE; +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; 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 index b927de057..265021e78 100644 --- 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 @@ -20,13 +20,13 @@ 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.grant.RuntimeContext; 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; 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 From 3f6b8cbb8313c43c48c979ede9fdb770caba381f Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Tue, 22 Oct 2024 16:32:52 +0200 Subject: [PATCH 13/15] feat: add retry on async resolution + pave the way for TTL renewal --- .../node/api/secrets/model/SecretMap.java | 6 +++- .../runtime/providers/ResolverService.java | 6 ++++ .../api/secrets/runtime/spec/Resolution.java | 3 +- .../runtime/spec/SpecLifecycleService.java | 2 +- .../services/runtimesecrets/Processor.java | 2 +- .../runtimesecrets/config/Config.java | 8 +++-- .../services/runtimesecrets/config/Retry.java | 8 +++++ .../services/runtimesecrets/el/Service.java | 7 +++- .../providers/DefaultResolverService.java | 33 ++++++++++++++++--- .../renewal/RenewalService.java | 2 +- .../spec/DefaultSpecLifecycleService.java | 19 ++++++++--- .../spring/RuntimeSecretsBeanFactory.java | 11 +++++-- .../spec/DefaultSpecLifecycleServiceTest.java | 2 +- 13 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Retry.java 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 6b3105a04..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 @@ -149,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/runtime/providers/ResolverService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/providers/ResolverService.java index 888a08950..9cb9868e2 100644 --- 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 @@ -2,8 +2,12 @@ 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) @@ -12,5 +16,7 @@ public interface ResolverService { Single resolve(String envId, SecretMount secretMount); + void resolveAsync(String envId, Spec spec, Duration delayBeforeResolve, @NonNull Action postResolution); + Single toSecretMount(String envId, SecretURL secretURL); } 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 index 04642d6b0..90a8410a7 100644 --- 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 @@ -6,9 +6,10 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record Resolution(Type type, Duration pollInterval) { +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/SpecLifecycleService.java b/gravitee-node-api/src/main/java/io/gravitee/node/api/secrets/runtime/spec/SpecLifecycleService.java index ca0d9cd5e..c6a5d53cb 100644 --- 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 @@ -8,7 +8,7 @@ */ public interface SpecLifecycleService { boolean shouldDeployOnTheFly(Ref ref); - Spec deployOnTheFly(String envId, Ref ref); + Spec deployOnTheFly(String envId, Ref ref, boolean retryOnError); void deploy(Spec spec); 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 index 6306f1f1d..3f098e972 100644 --- 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 @@ -76,7 +76,7 @@ public void onDefinitionDeploy(String envId, @Nonnull T definition, @Nullabl // get spec Spec spec = specRegistry.fromRef(context.envId(), context.ref()); if (spec == null && specLifecycleService.shouldDeployOnTheFly(context.ref())) { - spec = specLifecycleService.deployOnTheFly(envId, context.ref()); + spec = specLifecycleService.deployOnTheFly(envId, context.ref(), true); } if (context.ref().mainExpression().isLiteral()) { 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 index 0620b230e..a3790688b 100644 --- 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 @@ -19,14 +19,18 @@ * @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com) * @author GraviteeSource Team */ -public record Config(boolean denySpecWithoutACLs, OnTheFlySpecs onTheFlySpecs, Renewal renewal) { +public record Config(OnTheFlySpecs onTheFlySpecs, Retry retry, Renewal renewal) { public static final String CONFIG_PREFIX = "api.secrets"; - public static final String DENY_SPEC_WITHOUT_ACLS = CONFIG_PREFIX + ".denySpecWithoutACLs"; 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/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..21a1bcbf4 --- /dev/null +++ b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/config/Retry.java @@ -0,0 +1,8 @@ +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) {} 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 index 265021e78..9731ef9e5 100644 --- 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 @@ -48,6 +48,8 @@ public class Service { 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)) { @@ -95,7 +97,7 @@ public String fromELWithUri(String envId, String uriWithKey, String definitionKi Ref ref = uriAndKey.asRef(); Spec spec = specRegistry.getFromUriAndKey(envId, uriWithKey); if (spec == null && specLifecycleService.shouldDeployOnTheFly(ref)) { - spec = specLifecycleService.deployOnTheFly(envId, ref); + spec = specLifecycleService.deployOnTheFly(envId, ref, false); } return grantAndGet(envId, definitionKind, definitionId, spec, ref, uriAndKey.key()); } else { @@ -139,6 +141,9 @@ private Result toResult(Entry entry, String key) { case VALUE -> { Map secretMap = entry.value(); Secret secret = secretMap.get(key); + // TODO need: secretMount + // TODO need Secret to have expiration (not only map) + // TODO check expiration => call ResolverService.resolveAsync(mount, retry=true) if (secret != null) { result = new Result(Result.Type.VALUE, secret.asString()); } else { 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 index fc8b407f8..18e2dcbcd 100644 --- 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 @@ -15,34 +15,59 @@ */ package com.graviteesource.services.runtimesecrets.providers; +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.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 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; - public DefaultResolverService(SecretProviderRegistry secretProviderRegistry) { - this.secretProviderRegistry = secretProviderRegistry; - } - @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(String envId, Spec spec, Duration delayBeforeResolve, @NonNull Action postResolution) { + // TODO + } + + 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/renewal/RenewalService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/renewal/RenewalService.java index 4df3be4b3..457c0d0b5 100644 --- 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 @@ -129,6 +129,6 @@ public void stop() { } private void setupNextCheck(Spec spec) { - specsToRenew.put(spec, Instant.now().plus(spec.resolution().pollInterval())); + 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 index 84eeb3b45..84fd4788d 100644 --- 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 @@ -19,6 +19,7 @@ import com.graviteesource.services.runtimesecrets.config.Config; import com.graviteesource.services.runtimesecrets.renewal.RenewalService; +import io.gravitee.common.utils.RxHelper; 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; @@ -61,7 +62,7 @@ public boolean shouldDeployOnTheFly(Ref ref) { } @Override - public Spec deployOnTheFly(String envId, Ref ref) { + public Spec deployOnTheFly(String envId, Ref ref, boolean retryOnError) { Spec runtimeSpec = ref.asOnTheFlySpec(envId); specRegistry.register(runtimeSpec); cache.computeIfAbsent( @@ -77,7 +78,7 @@ public Spec deployOnTheFly(String envId, Ref ref) { .resolve(envId, mount) .doOnSuccess(entry -> { if (entry.type() == Entry.Type.ERROR) { - asyncResolution(runtimeSpec, config.onTheFlySpecs().onErrorRetryAfter(), () -> {}); + asyncResolution(runtimeSpec, config.onTheFlySpecs().onErrorRetryAfter(), retryOnError, () -> {}); } }) ) @@ -117,7 +118,7 @@ public void deploy(Spec spec) { } if (shouldResolve) { - asyncResolution(spec, Duration.ZERO, afterResolve); + asyncResolution(spec, Duration.ZERO, true, afterResolve); } } @@ -150,12 +151,22 @@ public void undeploy(Spec spec) { renewalService.onDelete(spec); } - private void asyncResolution(Spec spec, Duration delay, @NonNull Action postResolution) { + private void asyncResolution(Spec spec, Duration delay, boolean retryOnError, @NonNull Action postResolution) { SecretURL secretURL = spec.toSecretURL(); String envId = spec.envId(); + resolverService .toSecretMount(envId, secretURL) .delay(delay.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 -> resolverService.resolve(envId, mount)) .doOnError(err -> log.error("Async resolution failed", err)) 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 index 9853ebc21..9bf90b29a 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -61,20 +62,24 @@ public class RuntimeSecretsBeanFactory { @Bean Config config( - @Value("${" + DENY_SPEC_WITHOUT_ACLS + ":false}") boolean denySpecWithoutACLs, @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("${" + RENEWAL_CHECK_UNIT + ":MINUTES}") TimeUnit renewalCheckUnit, + @Value("${" + RETRY_ON_ERROR_ENABLED + ":true}") 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( - denySpecWithoutACLs, new OnTheFlySpecs( onTheFlySpecsEnabled, Duration.of(onTheFlySpecsOnErrorRetryAfterDelay, onTheFlySpecsOnErrorRetryAfterUnit.toChronoUnit()) ), + new Retry(retryOnErrorEnabled, retryOnErrorDelay, retryOnErrorUnit, factor, maxDelay), new Renewal(renewalEnabled, Duration.of(renewalCheckDelay, renewalCheckUnit.toChronoUnit())) ); } 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 index 6f216ec3e..ce0747ee3 100644 --- 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 @@ -109,7 +109,7 @@ void should_deploy_spec_and_get_secret_map_from_cache() { 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); + 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)); From b5fefdfd864b256873377b3fd7fac72e9d85360c Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Tue, 22 Oct 2024 20:29:12 +0200 Subject: [PATCH 14/15] fix: make test pass again --- .../services/runtimesecrets/config/Retry.java | 6 +++++- .../runtimesecrets/grant/DefaultGrantService.java | 12 +----------- .../spring/RuntimeSecretsBeanFactory.java | 2 +- .../services/runtimesecrets/ProcessorTest.java | 7 ++++++- .../services/runtimesecrets/el/ServiceTest.java | 3 ++- .../grant/DefaultGrantServiceTest.java | 3 ++- .../spec/DefaultSpecLifecycleServiceTest.java | 3 ++- 7 files changed, 19 insertions(+), 17 deletions(-) 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 index 21a1bcbf4..c5f282264 100644 --- 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 @@ -5,4 +5,8 @@ * @author GraviteeSource Team */ -public record Retry(boolean enabled, long delay, TimeUnit unit, float backoffFactor, int maxDelay) {} +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/grant/DefaultGrantService.java b/gravitee-node-secrets/gravitee-node-secrets-runtime/src/main/java/com/graviteesource/services/runtimesecrets/grant/DefaultGrantService.java index e32cc0645..e59021927 100644 --- 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 @@ -15,7 +15,6 @@ */ package com.graviteesource.services.runtimesecrets.grant; -import static com.graviteesource.services.runtimesecrets.config.Config.DENY_SPEC_WITHOUT_ACLS; 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; @@ -71,16 +70,7 @@ private boolean isGranted(DiscoveryContext context, Spec spec) { return false; } if (spec.acls() == null) { - if (config.denySpecWithoutACLs()) { - log.warn( - "secret spec for ref [{}] not granted because secrets requires ACLs. see conf: {}", - context.ref().rawRef(), - DENY_SPEC_WITHOUT_ACLS - ); - return false; - } else { - return checkSpec(context).test(spec); - } + return checkSpec(context).test(spec); } return checkSpec(context).test(spec) && checkACLs(context).test(spec.acls()); 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 index 9bf90b29a..0115405be 100644 --- 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 @@ -68,7 +68,7 @@ Config config( @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 + ":true}") boolean retryOnErrorEnabled, + @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, 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 index e72a2d3cb..c19a8a582 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -135,7 +136,11 @@ void before() { ); cache = new SimpleOffHeapCache(); - Config config = new Config(false, new OnTheFlySpecs(true, Duration.ofMillis(200)), new Renewal(true, Duration.ofMillis(200))); + 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(); 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 index 77ecc33a6..cb6da105a 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -92,7 +93,7 @@ void before() { null ); cache = new SimpleOffHeapCache(); - Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); + 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); 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 index 3da0d8664..3cc71618a 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -48,7 +49,7 @@ class DefaultGrantServiceTest { @BeforeEach void setup() { - Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); + Config config = new Config(new OnTheFlySpecs(true, Duration.ZERO), Retry.none(), new Renewal(true, Duration.ZERO)); this.cut = new DefaultGrantService(new GrantRegistry(), config); } 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 index ce0747ee3..5762aabb6 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -84,7 +85,7 @@ void before() { null ); cache = new SimpleOffHeapCache(); - Config config = new Config(false, new OnTheFlySpecs(true, Duration.ZERO), new Renewal(true, Duration.ZERO)); + 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( From e467193021bb7f4988fea7307003f3341d45b024 Mon Sep 17 00:00:00 2001 From: Benoit Bordigoni Date: Mon, 4 Nov 2024 11:08:51 +0100 Subject: [PATCH 15/15] fix: extract async resolution --- .../runtime/providers/ResolverService.java | 2 +- .../services/runtimesecrets/el/Service.java | 4 +-- .../providers/DefaultResolverService.java | 28 +++++++++++++-- .../spec/DefaultSpecLifecycleService.java | 35 ++++--------------- .../spring/RuntimeSecretsBeanFactory.java | 4 +-- .../runtimesecrets/ProcessorTest.java | 2 +- .../runtimesecrets/el/ServiceTest.java | 2 +- .../spec/DefaultSpecLifecycleServiceTest.java | 2 +- 8 files changed, 41 insertions(+), 38 deletions(-) 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 index 9cb9868e2..7dc705638 100644 --- 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 @@ -16,7 +16,7 @@ public interface ResolverService { Single resolve(String envId, SecretMount secretMount); - void resolveAsync(String envId, Spec spec, Duration delayBeforeResolve, @NonNull Action postResolution); + void resolveAsync(Spec spec, Duration delayBeforeResolve, boolean retryOnError, @NonNull Action postResolution); Single toSecretMount(String envId, SecretURL secretURL); } 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 index 9731ef9e5..41d8f8779 100644 --- 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 @@ -141,9 +141,9 @@ private Result toResult(Entry entry, String key) { case VALUE -> { Map secretMap = entry.value(); Secret secret = secretMap.get(key); - // TODO need: secretMount + // 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) - // TODO check expiration => call ResolverService.resolveAsync(mount, retry=true) if (secret != null) { result = new Result(Result.Type.VALUE, secret.asString()); } else { 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 index 18e2dcbcd..465ce8d91 100644 --- 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 @@ -15,18 +15,23 @@ */ 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; @@ -39,6 +44,8 @@ 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) { @@ -56,8 +63,25 @@ public Single resolve(String envId, SecretMount secretMount, Resolution r } @Override - public void resolveAsync(String envId, Spec spec, Duration delayBeforeResolve, @NonNull Action postResolution) { - // TODO + 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) { 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 index 84fd4788d..dbb236221 100644 --- 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 @@ -19,7 +19,6 @@ import com.graviteesource.services.runtimesecrets.config.Config; import com.graviteesource.services.runtimesecrets.renewal.RenewalService; -import io.gravitee.common.utils.RxHelper; 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; @@ -31,11 +30,9 @@ 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.functions.Action; import java.time.Duration; import java.util.Objects; -import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -78,7 +75,12 @@ public Spec deployOnTheFly(String envId, Ref ref, boolean retryOnError) { .resolve(envId, mount) .doOnSuccess(entry -> { if (entry.type() == Entry.Type.ERROR) { - asyncResolution(runtimeSpec, config.onTheFlySpecs().onErrorRetryAfter(), retryOnError, () -> {}); + resolverService.resolveAsync( + runtimeSpec, + config.onTheFlySpecs().onErrorRetryAfter(), + retryOnError, + () -> {} + ); } }) ) @@ -118,7 +120,7 @@ public void deploy(Spec spec) { } if (shouldResolve) { - asyncResolution(spec, Duration.ZERO, true, afterResolve); + resolverService.resolveAsync(spec, Duration.ZERO, true, afterResolve); } } @@ -150,27 +152,4 @@ public void undeploy(Spec spec) { cache.evict(CacheKey.from(spec)); renewalService.onDelete(spec); } - - private void asyncResolution(Spec spec, Duration delay, boolean retryOnError, @NonNull Action postResolution) { - SecretURL secretURL = spec.toSecretURL(); - String envId = spec.envId(); - - resolverService - .toSecretMount(envId, secretURL) - .delay(delay.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 -> resolverService.resolve(envId, mount)) - .doOnError(err -> log.error("Async resolution failed", err)) - .doFinally(postResolution) - .subscribe(entry -> cache.put(CacheKey.from(spec), entry)); - } } 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 index 0115405be..00e64eb03 100644 --- 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 @@ -174,8 +174,8 @@ SecretProviderDeployer runtimeSecretProviderDeployer( @Bean @Conditional({ AllowGraviteeYmlProviders.class }) - ResolverService resolverService(SecretProviderRegistry secretProviderRegistry) { - return new DefaultResolverService(secretProviderRegistry); + ResolverService resolverService(SecretProviderRegistry secretProviderRegistry, Cache cache, Config config) { + return new DefaultResolverService(secretProviderRegistry, cache, config); } @Bean 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 index c19a8a582..2e10ee6de 100644 --- 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 @@ -144,7 +144,7 @@ void before() { GrantService grantService = new DefaultGrantService(new GrantRegistry(), config); SpecRegistry specRegistry = new SpecRegistry(); ContextRegistry contextRegistry = new DefaultContextRegistry(); - ResolverService resolverService = new DefaultResolverService(secretProviderRegistry); + ResolverService resolverService = new DefaultResolverService(secretProviderRegistry, cache, config); renewalService = new RenewalService(resolverService, cache, config); specLifeCycleService = 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 index cb6da105a..3d6a51def 100644 --- 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 @@ -96,7 +96,7 @@ void before() { 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); + ResolverService resolverService = new DefaultResolverService(secretProviderRegistry, cache, config); RenewalService renewalService = new RenewalService(resolverService, cache, config); specLifeCycleService = new DefaultSpecLifecycleService( 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 index 5762aabb6..82e78e312 100644 --- 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 @@ -92,7 +92,7 @@ void before() { new SpecRegistry(), new DefaultContextRegistry(), cache, - new DefaultResolverService(registry), + new DefaultResolverService(registry, cache, config), new DefaultGrantService(new GrantRegistry(), config), renewalService, config