diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8767008709102a64029fc49b1576591b92326f68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +application-prod.properties +application-dev.properties + +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..40be386b981d8564ef04539edebb44dc6a389d39 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +include: + - project: osl/code/org.etsi.osl.main + ref: main + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "main"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "develop"' + + - project: osl/code/org.etsi.osl.main + ref: $CI_COMMIT_REF_NAME + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build_unprotected.yml + rules: + - if: '$CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop" && $CI_COMMIT_REF_PROTECTED == "false"' + +maven_build: + extends: .maven_build + +docker_build: + extends: .docker_build + needs: + - maven_build diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..93889a22b178ea7dba2cb9528cb7f67ef78d7675 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM ibm-semeru-runtimes:open-17.0.7_7-jdk + +RUN mkdir -p /opt/openslice/lib/ + +COPY target/secret-controller-0.1.0.jar /opt/openslice/lib/ +CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-jar", "/opt/openslice/lib/secret-controller-0.1.0.jar"] diff --git a/README.md b/README.md index 37eb62f791f8fea9cd1165f8b0612f74f13b0ba3..65245991b6d7c6aeec713189bb8b2ff9044cff1c 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,98 @@ -# org.etsi.osl.controllers.secrets +# Openslice secret controller +Generic controller for OpenSlice (ETSI OSL) that automatically stores secret +characteristics into Hashicorp Vault, an external secret management solution, +and replaces the original characteristic with a link to it. +## Configuration -## Getting started +The controller can be configured using the usual mechanisms provided by the +Spring framework, with the main options being the following: -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +```yaml +spring: + artemis: + broker-url: "tcp://artemis:61616" + user: artemis + password: password -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files -* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command: +vault: + uri: "http://vault:8200" + kvPath: "openslice" + token: "hvs.AAAAAAAAAA" ``` -cd existing_repo -git remote add origin https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.secrets.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -* [Set up project integrations](https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.secrets/-/settings/integrations) - -## Collaborate with your team - -* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/) -* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/) -* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically) -* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/) -* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/) - -## Test and Deploy -Use the built-in continuous integration in GitLab. +The `vault.uri` is the address of the Hashicorp Vault server to be used, the +`kvPath` is the path where the KV secrets engine for Openslice (explained in the +next section) was mounted, and `token` is the authentication token that allows +access to the KV secret store. -* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/) -* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/) -* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/) -* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/) -* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/) +## Vault setup -*** +In order for the controller to function the Vault must be configured to have +a KV secret engine version 2 mounted. By default this is expected to reside at +`openslice`, but it can be changed in the controller configuration. To create +the KV engine the following command may be used: -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +```sh +$ vault secrets enable -path openslice -version=2 -options=max_versions=1 kv +``` -## Suggestions for a good README +Then a policy and token can be created for the controller using the following +commands: -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +```sh +$ vault policy write openslice-policy - < + + + gitlab-maven + + + + Job-Token + ${CI_JOB_TOKEN} + + + + + + diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..eee85b35596bb3ed4ddffba50170c4ef48985e95 --- /dev/null +++ b/compose.yml @@ -0,0 +1,70 @@ +services: + osl-secrets-controller: + build: + context: . + dockerfile: Dockerfile + image: labs.etsi.org:5050/osl/code/addons/org.etsi.osl.controllers.secrets:develop + container_name: osl-secrets-controller + restart: always + profiles: ["dev", "prod"] + environment: + SPRING_ARTEMIS_BROKER_URL: tcp://anartemis:61616?jms.watchTopicAdvisories=false + SPRING_ARTEMIS_USER: artemis + SPRING_ARTEMIS_PASSWORD: artemis + VAULT_URI: http://osl-vault:8200 + VAULT_TOKEN: hvs.AAAAAAAAAAAA + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: INFO + logging: + driver: "json-file" + options: + max-size: "250m" + max-file: "2" + networks: + - compose_back + + + vault: + container_name: osl-vault + image: hashicorp/vault:2.0 + restart: always + profiles: ["dev"] + command: server + ports: + - "8200:8200" + cap_add: + - IPC_LOCK + volumes: + - vault_data:/vault/file + configs: + - source: vault_config + target: /vault/config/config.hcl + logging: + driver: "json-file" + options: + max-size: "200m" + max-file: "3" + networks: + - compose_back + +volumes: + vault_data: + +configs: + vault_config: + content: | + ui = true + api_addr = "http://osl-vault:8200" + disable_mlock = true + + storage "file" { + path = "/vault/file" + } + + listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = true + } + +networks: + compose_back: + external: true \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9a0d6a2ea876ba339faa64d7fda944443a50416 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + org.etsi.osl + secret-controller + 0.1.0 + org.etsi.osl.secret-controller + org.etsi.osl.secret-controller + + + + + + + + + + + + + + + + + 17 + 17 + 17 + 1.4.0-SNAPSHOT + + + + + etsi-gitlab-maven + etsi-gitlab-maven + https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven + + + + + + org.springframework.boot + spring-boot-starter-artemis + + + org.springframework.vault + spring-vault-core + 4.0.0 + + + org.springframework.boot + spring-boot-starter-json + + + + org.springframework.boot + spring-boot-starter-artemis-test + test + + + org.apache.activemq + artemis-jms-server + test + + + + org.etsi.osl + org.etsi.osl.model.tmf + ${org.etsi.osl.model.tmf.version} + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/org/etsi/osl/secretcontroller/Application.java b/src/main/java/org/etsi/osl/secretcontroller/Application.java new file mode 100644 index 0000000000000000000000000000000000000000..e008ac92419518b6391a4dd09490faf020dc480e --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/Application.java @@ -0,0 +1,31 @@ +package org.etsi.osl.secretcontroller; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.jms.annotation.EnableJms; +import tools.jackson.databind.ObjectMapper; + +@EnableJms +@SpringBootApplication( + // Spring vault core depends on Spring Data JPA which we do not use, but it causes auto configuration to try and + // create the database driver, which will fail because no database is used and therefore no properties + // configured. As such, we need to disable the auto configuration for everything related to Spring Data JPA. + exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class} +) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java new file mode 100644 index 0000000000000000000000000000000000000000..c5fbfcb6032c5587f0c16b557c7081b5a32e4473 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java @@ -0,0 +1,63 @@ +package org.etsi.osl.secretcontroller; + +import org.etsi.osl.tmf.so641.model.ServiceOrderUpdate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jms.core.JmsClient; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; + +@Service +public class CatalogClient { + private final JmsClient jmsClient; + private final ObjectMapper objectMapper; + + @Value("${topics.CATALOG_UPD_SERVICE}") + private String CATALOG_UPD_SERVICE; + + @Value("${topics.CATALOG_UPD_RESOURCE}") + private String CATALOG_UPD_RESOURCE; + + @Value("${topics.CATALOG_UPD_SERVICE_ORDER}") + private String CATALOG_UPD_SERVICE_ORDER; + + @Autowired + public CatalogClient(JmsClient jmsClient, ObjectMapper objectMapper) { + this.jmsClient = jmsClient; + this.objectMapper = objectMapper; + } + + public void updateService(String serviceId, org.etsi.osl.tmf.sim638.model.ServiceUpdate serviceUpdate) { + final var map = new HashMap(); + map.put("serviceId", serviceId); + map.put("triggerServiceActionQueue", false); + + final var payload = this.objectMapper.writeValueAsString(serviceUpdate); + + jmsClient.destination(CATALOG_UPD_SERVICE) + .send(payload, map); + } + + public void updateResource(String resourceId, org.etsi.osl.tmf.ri639.model.ResourceUpdate resourceUpdate) { + final var map = new HashMap(); + map.put("resourceId", resourceId); + map.put("triggerServiceActionQueue", false); + + final var payload = this.objectMapper.writeValueAsString(resourceUpdate); + + jmsClient.destination(CATALOG_UPD_RESOURCE) + .send(payload, map); + } + + public void updateServiceOrder(String orderId, ServiceOrderUpdate svcOrderUpdate) { + final var map = new HashMap(); + map.put("orderid", orderId); + + final var payload = this.objectMapper.writeValueAsString(svcOrderUpdate); + + jmsClient.destination(CATALOG_UPD_SERVICE_ORDER) + .send(payload, map); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java b/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java new file mode 100644 index 0000000000000000000000000000000000000000..7eec9a06acca954030e29a13e014c25c4ac62ed5 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java @@ -0,0 +1,75 @@ +package org.etsi.osl.secretcontroller; + +import org.etsi.osl.secretcontroller.config.RulesConfig; +import org.etsi.osl.secretcontroller.rules.ResourceRule; +import org.etsi.osl.secretcontroller.rules.RuleEvaluatorService; +import org.etsi.osl.tmf.common.model.EValueType; +import org.etsi.osl.tmf.ri639.model.Characteristic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +import java.util.Optional; + +@Service +public class ResourceRulesService { + private static final Logger log = LoggerFactory.getLogger(ResourceRulesService.class); + + private final RulesConfig rulesConfig; + private final RuleEvaluatorService ruleEvaluatorService; + private final ObjectMapper mapper; + + @Autowired + public ResourceRulesService(RulesConfig rulesConfig, RuleEvaluatorService ruleEvaluatorService, + ObjectMapper mapper) { + this.rulesConfig = rulesConfig; + this.ruleEvaluatorService = ruleEvaluatorService; + this.mapper = mapper; + } + + public ResourceRulesApplier getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) { + ResourceRule rule = null; + + final var cat = resource.getCategory(); + if (cat != null) + rule = this.rulesConfig.getResourceRules().get(cat); + + return new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource, + Optional.ofNullable(rule)); + } + + public static class ResourceRulesApplier { + private final Optional rule; + private final RuleEvaluatorService ruleEvaluatorService; + + public ResourceRulesApplier(ObjectMapper mapper, + RuleEvaluatorService ruleEvaluatorService, + org.etsi.osl.tmf.ri639.model.Resource resource, + Optional rule) { + this.rule = rule; + this.ruleEvaluatorService = ruleEvaluatorService; + } + + public SecretApplyResult saveCharacteristic(Characteristic characteristic) { + final var name = characteristic.getName(); + final var valueType = characteristic.getValueType(); + final var value = characteristic.getValue().getValue(); + + if (this.rule.isPresent()) { + for (final var cRule : this.rule.get().getCharacteristics()) { + final var m = cRule.getMatch().matcher(name); + if (!m.matches()) + continue; + + return this.ruleEvaluatorService.evaluateRule(value, cRule); + } + } else if (valueType != null && valueType.equals(EValueType.SECRET.getValue())) { + return SecretApplyResult.replace(value); + } + + return SecretApplyResult.ignore(); + } + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/SecretApplyResult.java b/src/main/java/org/etsi/osl/secretcontroller/SecretApplyResult.java new file mode 100644 index 0000000000000000000000000000000000000000..c4db983107b56192fb16aac59b18f9ac7c70f038 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/SecretApplyResult.java @@ -0,0 +1,35 @@ +package org.etsi.osl.secretcontroller; + +public class SecretApplyResult { + public final Action action; + private final String value; + + private SecretApplyResult(Action action, String value) { + this.action = action; + this.value = value; + } + + public static SecretApplyResult replace(String value) { + return new SecretApplyResult(Action.Replace, value); + } + + public static SecretApplyResult erase() { + return new SecretApplyResult(Action.Erase, null); + } + + public static SecretApplyResult ignore() { + return new SecretApplyResult(Action.Ignore, null); + } + + public String getValue() { + if (this.action != Action.Replace) + throw new IllegalStateException("Value is only available when replacing"); + return this.value; + } + + public enum Action { + Replace, + Erase, + Ignore, + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java new file mode 100644 index 0000000000000000000000000000000000000000..e328e44ece727c2c2247a617f68d3834acfa41b8 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java @@ -0,0 +1,65 @@ +package org.etsi.osl.secretcontroller; + +import org.etsi.osl.secretcontroller.config.RulesConfig; +import org.etsi.osl.secretcontroller.rules.RuleEvaluatorService; +import org.etsi.osl.secretcontroller.rules.ServiceRule; +import org.etsi.osl.tmf.common.model.EValueType; +import org.etsi.osl.tmf.common.model.service.Characteristic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ServiceRulesService { + private final RulesConfig rulesConfig; + private final RuleEvaluatorService ruleEvaluatorService; + + @Autowired + public ServiceRulesService(RulesConfig rulesConfig, RuleEvaluatorService ruleEvaluatorService) { + this.rulesConfig = rulesConfig; + this.ruleEvaluatorService = ruleEvaluatorService; + } + + public ServiceRulesApplier getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) { + final var name = svc.getName(); + final var rule = this.rulesConfig.getServiceRules().get(name); + return new ServiceRulesApplier(this.ruleEvaluatorService, Optional.ofNullable(rule)); + } + + public ServiceRulesApplier getServiceRules(org.etsi.osl.tmf.so641.model.ServiceRestriction svc) { + final var name = svc.getName(); + final var rule = this.rulesConfig.getServiceRules().get(name); + return new ServiceRulesApplier(this.ruleEvaluatorService, Optional.ofNullable(rule)); + } + + public static class ServiceRulesApplier { + private final Optional rule; + private final RuleEvaluatorService ruleEvaluatorService; + + public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, Optional rule) { + this.rule = rule; + this.ruleEvaluatorService = ruleEvaluatorService; + } + + public SecretApplyResult saveCharacteristic(Characteristic characteristic) { + final var name = characteristic.getName(); + final var valueType = characteristic.getValueType(); + final var value = characteristic.getValue().getValue(); + + if (this.rule.isPresent()) { + for (final var cRule : this.rule.get().characteristics()) { + final var m = cRule.getMatch().matcher(name); + if (!m.matches()) + continue; + + return this.ruleEvaluatorService.evaluateRule(value, cRule); + } + } else if (valueType != null && valueType.equals(EValueType.SECRET.getValue())) { + return SecretApplyResult.replace(value); + } + + return SecretApplyResult.ignore(); + } + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/VaultService.java b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java new file mode 100644 index 0000000000000000000000000000000000000000..12a7535e89a3cda094186a11bd8d03089e2dbd65 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java @@ -0,0 +1,76 @@ +package org.etsi.osl.secretcontroller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.vault.core.SecretNotFoundException; +import org.springframework.vault.core.VaultKeyValueOperations; +import org.springframework.vault.core.VaultKeyValueOperationsSupport; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.web.util.UriUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; + +@Service +public class VaultService { + private final String vaultBaseSecretUri; + private final VaultKeyValueOperations vaultKV; + + @Autowired + public VaultService( + VaultTemplate vaultTemplate, + @Value("${vault.uri}") String vaultUri, + @Value("${vault.kvPath}") String kvPath + ) { + this.vaultKV = vaultTemplate.opsForKeyValue(kvPath, VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); + + if (vaultUri.endsWith("/")) + vaultUri = vaultUri.substring(0, vaultUri.length() - 1); + + this.vaultBaseSecretUri = String.format("%s/v1/%s/data/", vaultUri, kvPath); + } + + public String getVaultKey(String namespace, String... keys) { + return String.format("%s/%s", namespace, String.join("/", keys)); + } + + public String getVaultUri(String secretKey) { + return vaultBaseSecretUri + UriUtils.encodePath(secretKey, StandardCharsets.US_ASCII); + } + + public boolean isAlreadySecret(String value) { + return value.startsWith(this.vaultBaseSecretUri); + } + + public Optional extractVaultKeyFromUri(String secretUri) { + if (!secretUri.startsWith(this.vaultBaseSecretUri)) + return Optional.empty(); + return Optional.of(secretUri.substring(this.vaultBaseSecretUri.length())); + } + + public Optional> getSecret(String key) { + final var secret = this.vaultKV.get(key); + if (secret == null) + return Optional.empty(); + + final var data = secret.getData(); + if (data == null) + return Optional.empty(); + + return Optional.of(data); + } + + public void upsertSecret(String key, Map data) { + try { + this.vaultKV.patch(key, data); + } catch (SecretNotFoundException e) { + this.vaultKV.put(key, data); + } + } + + public void deleteSecret(String key) { + this.vaultKV.delete(key); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..0290cdea86e7f604f1da398896eb77a7def258c2 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java @@ -0,0 +1,19 @@ +package org.etsi.osl.secretcontroller.config; + +import jakarta.jms.ConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; + +@Configuration +public class ActiveMQConfiguration { + + @Bean(name = "topicJmsListenerContainerFactory") + public DefaultJmsListenerContainerFactory topicJmsListenerContainerFactory(ConnectionFactory connectionFactory) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setPubSubDomain(true); + return factory; + } + +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/config/RulesConfig.java b/src/main/java/org/etsi/osl/secretcontroller/config/RulesConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..50113fb2675b73e41f997c3ec575c79410868cae --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/config/RulesConfig.java @@ -0,0 +1,34 @@ +package org.etsi.osl.secretcontroller.config; + +import org.etsi.osl.secretcontroller.rules.ResourceRule; +import org.etsi.osl.secretcontroller.rules.ServiceRule; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "app") +public class RulesConfig { + private Map serviceRules = Map.of(); + private Map resourceRules = Map.of(); + + public Map getServiceRules() { + return serviceRules; + } + + public void setServiceRules(Map serviceRules) { + this.serviceRules = serviceRules; + } + + public Map getResourceRules() { + return resourceRules; + } + + public void setResourceRules(Map resourceRules) { + this.resourceRules = resourceRules; + } + +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..d87811f579272fd3ccba1735a6597fcc2e0ce155 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java @@ -0,0 +1,10 @@ +package org.etsi.osl.secretcontroller.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.vault.config.EnvironmentVaultConfiguration; + +@Configuration +@Import(EnvironmentVaultConfiguration.class) +public class VaultConfiguration { +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e1ab2f31858fb796c0e7d5359cf148252e0fc2e2 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java @@ -0,0 +1,71 @@ +package org.etsi.osl.secretcontroller.listeners; + +import org.etsi.osl.secretcontroller.VaultService; +import org.etsi.osl.secretcontroller.listeners.commands.UnsealCharacteristicCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.jms.core.JmsClient; +import org.springframework.jms.support.JmsHeaders; +import org.springframework.jms.support.JmsMessageHeaderAccessor; +import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; + +@Component +public class ControllerCommandsListener { + private static final Logger log = LoggerFactory.getLogger(ControllerCommandsListener.class); + private final VaultService vaultService; + private final JmsClient jmsClient; + private final ObjectMapper objectMapper; + + @Autowired + public ControllerCommandsListener(VaultService vaultService, JmsClient jmsClient, ObjectMapper objectMapper) { + this.vaultService = vaultService; + this.jmsClient = jmsClient; + this.objectMapper = objectMapper; + } + + @JmsListener(destination = "${topics.SECRETS_CHARACTERISTIC_UNSEAL}") + public void processUnsealCommand(String content, JmsMessageHeaderAccessor jmsHeaderAccessor) { + final var destination = jmsHeaderAccessor.getReplyTo(); + if (destination == null) { + log.warn("Unseal request: no reply destination"); + return; + } + + final var cmd = this.objectMapper.readValue(content, UnsealCharacteristicCommand.class); + + final var vaultKeyOpt = this.vaultService.extractVaultKeyFromUri(cmd.secretURI()); + if (vaultKeyOpt.isEmpty()) { + // Ignore this command as the URI isn't for this controller + log.debug("Unseal request: secret URI does not belong to this controller"); + return; + } + final var vaultKey = vaultKeyOpt.get(); + + Object result = null; + + final var secretData = this.vaultService.getSecret(vaultKey); + if (secretData.isPresent()) { + final var characteristicValue = secretData.get().get(cmd.characteristic()); + if (characteristicValue != null) { + result = characteristicValue; + } else { + log.debug("Unseal request: Characteristic not found"); + } + } else { + log.debug("Unseal request: Secret not found"); + } + + final var headers = new HashMap(); + final var correlationId = jmsHeaderAccessor.getCorrelationId(); + if (correlationId != null) + headers.put(JmsHeaders.CORRELATION_ID, correlationId); + + this.jmsClient.destination(destination) + .send(this.objectMapper.writeValueAsString(result), headers); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java new file mode 100644 index 0000000000000000000000000000000000000000..64b0c27fb346f7444f930baa128ac7b9c59b0bd9 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java @@ -0,0 +1,95 @@ +package org.etsi.osl.secretcontroller.listeners; + +import org.etsi.osl.secretcontroller.CatalogClient; +import org.etsi.osl.secretcontroller.ResourceRulesService; +import org.etsi.osl.secretcontroller.SecretApplyResult; +import org.etsi.osl.secretcontroller.VaultService; +import org.etsi.osl.tmf.ri639.model.Resource; +import org.etsi.osl.tmf.ri639.model.ResourceAttributeValueChangeNotification; +import org.etsi.osl.tmf.ri639.model.ResourceCreateNotification; +import org.etsi.osl.tmf.ri639.model.ResourceUpdate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; + +@Component +public class ResourceEventsListener { + private static final String SECRET_NAMESPACE = "resource"; + + private final VaultService vaultService; + + private final CatalogClient catalogClient; + private final ResourceRulesService resourceRulesService; + + private final ObjectMapper objectMapper; + + @Autowired + public ResourceEventsListener( + CatalogClient catalogClient, + ResourceRulesService resourceRulesService, + VaultService vaultService, + ObjectMapper objectMapper + ) { + this.vaultService = vaultService; + this.catalogClient = catalogClient; + this.resourceRulesService = resourceRulesService; + this.objectMapper = objectMapper; + } + + @JmsListener(destination = "${topics.EVENT_RESOURCE_CREATE}", containerFactory = "topicJmsListenerContainerFactory") + public void processResourceCreate(String content) { + final var res = this.objectMapper.readValue(content, ResourceCreateNotification.class); + this.processResource(res.getEvent().getEvent().getResource()); + } + + @JmsListener(destination = "${topics.EVENT_RESOURCE_ATTRIBUTE_VALUE_CHANGED}", containerFactory = + "topicJmsListenerContainerFactory") + public void processResourceAttributeChange(String content) { + final var res = this.objectMapper.readValue(content, ResourceAttributeValueChangeNotification.class); + this.processResource(res.getEvent().getEvent().getResource()); + } + + protected void processResource(Resource resource) { + final var ruleApplier = this.resourceRulesService.getResourceRules(resource); + + final var resourceUpdate = new ResourceUpdate(); + final var secretData = new HashMap(); + + final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, resource.getId()); + final var secretURI = this.vaultService.getVaultUri(vaultKey); + + var dirty = false; + for (final var characteristic : resource.getResourceCharacteristic()) { + // Characteristic was already stored, ignore it + if (this.vaultService.isAlreadySecret(characteristic.getValue().getValue())) + continue; + + final var storeSecret = ruleApplier.saveCharacteristic(characteristic); + if (storeSecret.action != SecretApplyResult.Action.Ignore) + dirty = true; + + switch (storeSecret.action) { + case Replace -> { + secretData.put(characteristic.getName(), storeSecret.getValue()); + characteristic.getValue().setValue(secretURI); + resourceUpdate.addResourceCharacteristicItem(characteristic); + } + case Erase -> { + characteristic.getValue().setValue("Erased by secret controller"); + resourceUpdate.addResourceCharacteristicItem(characteristic); + } + } + } + + if (!dirty) + return; + + if (!secretData.isEmpty()) + this.vaultService.upsertSecret(vaultKey, secretData); + + catalogClient.updateResource(resource.getId(), resourceUpdate); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java new file mode 100644 index 0000000000000000000000000000000000000000..0f65c0cde2bd868fbb40eef861d2d66b9ece19de --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java @@ -0,0 +1,102 @@ +package org.etsi.osl.secretcontroller.listeners; + +import org.etsi.osl.secretcontroller.CatalogClient; +import org.etsi.osl.secretcontroller.SecretApplyResult; +import org.etsi.osl.secretcontroller.ServiceRulesService; +import org.etsi.osl.secretcontroller.VaultService; +import org.etsi.osl.tmf.sim638.model.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; + +@Component +public class ServiceEventsListener { + private static final String SECRET_NAMESPACE = "service"; + + private final VaultService vaultService; + + private final CatalogClient catalogClient; + private final ServiceRulesService serviceRulesService; + + private final ObjectMapper objectMapper; + + @Autowired + public ServiceEventsListener( + VaultService vaultService, + CatalogClient catalogClient, + ServiceRulesService serviceRulesService, + ObjectMapper objectMapper + ) { + this.vaultService = vaultService; + this.catalogClient = catalogClient; + this.serviceRulesService = serviceRulesService; + this.objectMapper = objectMapper; + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_CREATE}", containerFactory = "topicJmsListenerContainerFactory") + public void processServiceCreate(String content) { + final var res = this.objectMapper.readValue(content, ServiceCreateNotification.class); + this.processService(res.getEvent().getService()); + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_ATTRIBUTE_VALUE_CHANGED}", containerFactory = + "topicJmsListenerContainerFactory") + public void processServiceAttributeChange(String content) { + final var res = this.objectMapper.readValue(content, ServiceAttributeValueChangeNotification.class); + this.processService(res.getEvent().getService()); + } + + protected void processService(Service svc) { + final var ruleApplier = this.serviceRulesService.getServiceRules(svc); + + final var serviceUpdate = new ServiceUpdate(); + final var secretData = new HashMap(); + + final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, svc.getId()); + final var secretURI = this.vaultService.getVaultUri(vaultKey); + + var dirty = false; + for (final var characteristic : svc.getServiceCharacteristic()) { + // Characteristic was already stored, ignore it + if (this.vaultService.isAlreadySecret(characteristic.getValue().getValue())) + continue; + + final var storeSecret = ruleApplier.saveCharacteristic(characteristic); + if (storeSecret.action != SecretApplyResult.Action.Ignore) + dirty = true; + + switch (storeSecret.action) { + case Replace -> { + secretData.put(characteristic.getName(), storeSecret.getValue()); + characteristic.getValue().setValue(secretURI); + serviceUpdate.addServiceCharacteristicItem(characteristic); + } + case Erase -> { + characteristic.getValue().setValue("Erased by secret controller"); + serviceUpdate.addServiceCharacteristicItem(characteristic); + } + } + } + + if (!dirty) + return; + + if (!secretData.isEmpty()) + this.vaultService.upsertSecret(vaultKey, secretData); + + catalogClient.updateService(svc.getId(), serviceUpdate); + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_DELETE}", containerFactory = + "topicJmsListenerContainerFactory") + public void processServiceDelete(String content) { + final var res = this.objectMapper.readValue(content, ServiceDeleteNotification.class); + final var svc = res.getEvent().getService(); + + final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, svc.getId()); + this.vaultService.deleteSecret(vaultKey); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceOrderEventsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceOrderEventsListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ee11654faa1719caec5a59b2fb558c6570e644d3 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceOrderEventsListener.java @@ -0,0 +1,122 @@ +package org.etsi.osl.secretcontroller.listeners; + +import org.etsi.osl.secretcontroller.CatalogClient; +import org.etsi.osl.secretcontroller.SecretApplyResult; +import org.etsi.osl.secretcontroller.ServiceRulesService; +import org.etsi.osl.secretcontroller.VaultService; +import org.etsi.osl.tmf.so641.model.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; + +@Component +public class ServiceOrderEventsListener { + private static final String SECRET_NAMESPACE = "service-order"; + + private final VaultService vaultService; + + private final CatalogClient catalogClient; + private final ServiceRulesService serviceRulesService; + + private final ObjectMapper objectMapper; + + @Autowired + public ServiceOrderEventsListener( + VaultService vaultService, + CatalogClient catalogClient, + ServiceRulesService serviceRulesService, + ObjectMapper objectMapper + ) { + this.vaultService = vaultService; + this.catalogClient = catalogClient; + this.serviceRulesService = serviceRulesService; + this.objectMapper = objectMapper; + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_ORDER_CREATE}", containerFactory = + "topicJmsListenerContainerFactory") + public void processServiceCreate(String content) { + final var res = this.objectMapper.readValue(content, ServiceOrderCreateNotification.class); + this.processServiceOrder(res.getEvent().getServiceOrder()); + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_ORDER_ATTRIBUTE_VALUE_CHANGED}", containerFactory = + "topicJmsListenerContainerFactory") + public void processServiceAttributeChange(String content) { + final var res = this.objectMapper.readValue(content, ServiceOrderAttributeValueChangeNotification.class); + this.processServiceOrder(res.getEvent().getServiceOrder()); + } + + private void processServiceOrder(ServiceOrder serviceOrder) { + final var svcOrderUpdate = new ServiceOrderUpdate(); + + var isDirty = false; + for (final var orderItem : serviceOrder.getOrderItem()) { + if (orderItem.getService() == null) + continue; + + final var ruleApplier = this.serviceRulesService.getServiceRules(orderItem.getService()); + final var svc = new ServiceRestriction(); + final var secretData = new HashMap(); + + final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, "services", orderItem.getId()); + final var secretURI = this.vaultService.getVaultUri(vaultKey); + + var svcIsDirty = false; + for (final var characteristic : orderItem.getService().getServiceCharacteristic()) { + // Characteristic was already stored, ignore it + if (this.vaultService.isAlreadySecret(characteristic.getValue().getValue())) + continue; + + final var storeSecret = ruleApplier.saveCharacteristic(characteristic); + if (storeSecret.action != SecretApplyResult.Action.Ignore) + svcIsDirty = true; + + switch (storeSecret.action) { + case Replace -> { + secretData.put(characteristic.getName(), storeSecret.getValue()); + characteristic.getValue().setValue(secretURI); + svc.addServiceCharacteristicItem(characteristic); + } + case Erase -> { + characteristic.getValue().setValue("Erased by secret controller"); + svc.addServiceCharacteristicItem(characteristic); + } + } + } + + if (!svcIsDirty) + continue; + + if (!secretData.isEmpty()) + this.vaultService.upsertSecret(vaultKey, secretData); + + final var updOrderItem = new ServiceOrderItem(); + updOrderItem.setUuid(updOrderItem.getUuid()); + updOrderItem.setService(svc); + + svcOrderUpdate.addOrderItemItem(orderItem); + isDirty = true; + } + + if (!isDirty) + return; + + this.catalogClient.updateServiceOrder(serviceOrder.getId(), svcOrderUpdate); + } + + @JmsListener(destination = "${topics.EVENT_SERVICE_ORDER_DELETE}", containerFactory = + "topicJmsListenerContainerFactory") + public void processServiceDelete(String content) { + final var res = this.objectMapper.readValue(content, ServiceOrderDeleteNotification.class); + final var svcOrder = res.getEvent().getServiceOrder(); + + for (final var orderItem : svcOrder.getOrderItem()) { + final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, "services", orderItem.getId()); + this.vaultService.deleteSecret(vaultKey); + } + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/commands/UnsealCharacteristicCommand.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/commands/UnsealCharacteristicCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..14ec04891293f162c5966a0f6d2b7f7ab7e016ea --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/commands/UnsealCharacteristicCommand.java @@ -0,0 +1,4 @@ +package org.etsi.osl.secretcontroller.listeners.commands; + +public record UnsealCharacteristicCommand(String secretURI, String characteristic) { +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java b/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java new file mode 100644 index 0000000000000000000000000000000000000000..f97a236ea1704414b3c9855bd46f894d6d9954e2 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java @@ -0,0 +1,34 @@ +package org.etsi.osl.secretcontroller.rules; + +import org.etsi.osl.secretcontroller.SecretApplyResult; + +import java.util.regex.Pattern; + +public final class CharacteristicRule { + private Pattern match; + private SecretApplyResult.Action action = SecretApplyResult.Action.Replace; + + public Pattern getMatch() { + return match; + } + + public void setMatch(Pattern match) { + this.match = match; + } + + public SecretApplyResult.Action getAction() { + return action; + } + + public void setAction(SecretApplyResult.Action action) { + this.action = action; + } + + @Override + public String toString() { + return "CharacteristicRule[" + + "match=\"" + match + "\"" + + ", action=" + action + + ']'; + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java b/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java new file mode 100644 index 0000000000000000000000000000000000000000..c19e2267e5b79047c8781c5b6c4197f8432fa40b --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java @@ -0,0 +1,22 @@ +package org.etsi.osl.secretcontroller.rules; + +import java.util.List; + +public final class ResourceRule { + private List characteristics = List.of(); + + public List getCharacteristics() { + return characteristics; + } + + public void setCharacteristics(List characteristics) { + this.characteristics = characteristics; + } + + @Override + public String toString() { + return "ResourceRule[" + + "characteristics=" + characteristics + + ']'; + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java b/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java new file mode 100644 index 0000000000000000000000000000000000000000..f034c6a3dc42ead58368e90b5fa30af4f7241346 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java @@ -0,0 +1,15 @@ +package org.etsi.osl.secretcontroller.rules; + +import org.etsi.osl.secretcontroller.SecretApplyResult; +import org.springframework.stereotype.Service; + +@Service +public class RuleEvaluatorService { + public final SecretApplyResult evaluateRule(String value, CharacteristicRule rule) { + return switch (rule.getAction()) { + case Replace -> SecretApplyResult.replace(value); + case Erase -> SecretApplyResult.erase(); + case Ignore -> SecretApplyResult.ignore(); + }; + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java b/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java new file mode 100644 index 0000000000000000000000000000000000000000..0094a397ec5c51f481b35f6c64de86562660f930 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java @@ -0,0 +1,22 @@ +package org.etsi.osl.secretcontroller.rules; + +import java.util.List; + +public final class ServiceRule { + private List characteristics = List.of(); + + public ServiceRule(List characteristics) { + this.characteristics = characteristics; + } + + public List characteristics() { + return characteristics; + } + + @Override + public String toString() { + return "ServiceRule[" + + "characteristics=" + characteristics + ']'; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..25ae2619f5b6acc25cec4f14591d1abe94952dcb --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,33 @@ +spring: + application: + name: "org.etsi.osl.secret-controller" + + artemis: + mode: "native" + broker-url: "tcp://localhost:61616?jms.watchTopicAdvisories=false" + +vault: + uri: "http://localhost:8200" + kvPath: "openslice" + +topics: + SECRETS_CHARACTERISTIC_UNSEAL: "SECRETS.CHARACTERISTIC.UNSEAL" + + CATALOG_UPD_SERVICE: "CATALOG.UPD.SERVICE" + EVENT_SERVICE_CREATE: "EVENT.SERVICE.CREATE" + EVENT_SERVICE_ATTRIBUTE_VALUE_CHANGED: "EVENT.SERVICE.ATTRCHANGED" + EVENT_SERVICE_DELETE: "EVENT.SERVICE.DELETE" + + CATALOG_UPD_RESOURCE: "CATALOG.UPD.RESOURCE" + EVENT_RESOURCE_CREATE: "EVENT.RESOURCE.CREATE" + EVENT_RESOURCE_ATTRIBUTE_VALUE_CHANGED: "EVENT.RESOURCE.ATTRCHANGED" + EVENT_RESOURCE_DELETE: "EVENT.RESOURCE.DELETE" + + CATALOG_UPD_SERVICE_ORDER: "CATALOG.UPD.SERVICEORDER_BY_ID" + EVENT_SERVICE_ORDER_CREATE: "EVENT.SERVICEORDER.CREATE" + EVENT_SERVICE_ORDER_ATTRIBUTE_VALUE_CHANGED: "EVENT.SERVICEORDER.ATTRCHANGED" + EVENT_SERVICE_ORDER_DELETE: "EVENT.SERVICEORDER.DELETE" + +app: + service-rules: { } + resource-rules: { } diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000000000000000000000000000000000000..74229cd400fb31f76673214d76c0256f438e4ccc --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,11 @@ + ___ ____ _ _ + / _ \ _ __ ___ _ __ / ___|| (_) ___ ___ + | | | | '_ \ / _ \ '_ \\___ \| | |/ __/ _ \ + | |_| | |_) | __/ | | |___) | | | (_| __/ + \___/| .__/ \___|_| |_|____/|_|_|\___\___| + |_| + __ __________________ + / / __ __ / __/_ __/ __/ _/ + / _ \/ // / / _/ / / _\ \_/ / + /_.__/\_, / /___/ /_/ /___/___/ + /___/ \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..e797aa8a30f74e322f2fd6f95f0208b6f28da729 --- /dev/null +++ b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java @@ -0,0 +1,15 @@ +package org.etsi.osl.secretcontroller; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListenerTest.java b/src/test/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListenerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b6e844d1b9c523758560b34f21aafd5cb22c6733 --- /dev/null +++ b/src/test/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListenerTest.java @@ -0,0 +1,120 @@ +package org.etsi.osl.secretcontroller.listeners; + +import jakarta.jms.*; +import org.etsi.osl.secretcontroller.VaultService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import tools.jackson.databind.ObjectMapper; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestPropertySource(properties = { + "spring.artemis.mode=embedded", + "spring.artemis.embedded.persistent=false", + "spring.artemis.embedded.queues=SECRETS.CHARACTERISTIC.UNSEAL", + "topics.SECRETS_CHARACTERISTIC_UNSEAL=SECRETS.CHARACTERISTIC.UNSEAL", + "vault.token=test-token" +}) +@ActiveProfiles("test") +class ControllerCommandsListenerTest { + + @Autowired + private JmsTemplate jmsTemplate; + + @Autowired + private ConnectionFactory connectionFactory; + + @MockitoBean + private VaultService vaultService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void processUnsealCommand_whenSecretUriNotBelonging_shouldNotSendResponse() throws Exception { + when(vaultService.extractVaultKeyFromUri(anyString())).thenReturn(Optional.empty()); + + try (Connection connection = connectionFactory.createConnection(); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + connection.start(); + TemporaryQueue replyQueue = session.createTemporaryQueue(); + MessageConsumer consumer = session.createConsumer(replyQueue); + + String content = objectMapper + .writeValueAsString(Map.of("secretURI", "http://some-uri", "characteristic", "password")); + jmsTemplate.convertAndSend("SECRETS.CHARACTERISTIC.UNSEAL", content, msg -> { + msg.setJMSReplyTo(replyQueue); + msg.setJMSCorrelationID("test-corr-1"); + return msg; + }); + + Message response = consumer.receive(1000); + assertNull(response); + } + } + + @Test + void processUnsealCommand_whenSecretNotFound_shouldSendNullResponse() throws Exception { + when(vaultService.extractVaultKeyFromUri(anyString())).thenReturn(Optional.of("key")); + when(vaultService.getSecret(anyString())).thenReturn(Optional.empty()); + + try (Connection connection = connectionFactory.createConnection(); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + connection.start(); + TemporaryQueue replyQueue = session.createTemporaryQueue(); + MessageConsumer consumer = session.createConsumer(replyQueue); + + String content = objectMapper + .writeValueAsString(Map.of("secretURI", "http://vault/prefix/key", "characteristic", "password")); + jmsTemplate.convertAndSend("SECRETS.CHARACTERISTIC.UNSEAL", content, msg -> { + msg.setJMSReplyTo(replyQueue); + msg.setJMSCorrelationID("test-corr-2"); + return msg; + }); + + Message response = consumer.receive(5000); + assertNotNull(response); + assertInstanceOf(TextMessage.class, response); + assertEquals("null", ((TextMessage) response).getText()); + assertEquals("test-corr-2", response.getJMSCorrelationID()); + } + } + + @Test + void processUnsealCommand_whenSecretFoundAndCharacteristicExists_shouldSendCharacteristicValue() throws Exception { + when(vaultService.extractVaultKeyFromUri(anyString())).thenReturn(Optional.of("key")); + when(vaultService.getSecret(anyString())).thenReturn(Optional.of(Map.of("password", "supersecret"))); + + try (Connection connection = connectionFactory.createConnection(); + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { + connection.start(); + TemporaryQueue replyQueue = session.createTemporaryQueue(); + MessageConsumer consumer = session.createConsumer(replyQueue); + + String content = objectMapper + .writeValueAsString(Map.of("secretURI", "http://vault/prefix/key", "characteristic", "password")); + jmsTemplate.convertAndSend("SECRETS.CHARACTERISTIC.UNSEAL", content, msg -> { + msg.setJMSReplyTo(replyQueue); + msg.setJMSCorrelationID("test-corr-3"); + return msg; + }); + + Message response = consumer.receive(5000); + assertNotNull(response); + assertInstanceOf(TextMessage.class, response); + assertEquals(objectMapper.writeValueAsString("supersecret"), ((TextMessage) response).getText()); + assertEquals("test-corr-3", response.getJMSCorrelationID()); + } + } + +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..704003e8142296cf55a60941fdb257551f8a6403 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,8 @@ +spring: + artemis: + user: "artemis" + password: "artemis" + +vault: + token: "hvs.AAAAAAAAAAAAA" + \ No newline at end of file