From ff61d676744d926d7bc7edf3da8bb734fcf9aa21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Wed, 3 Dec 2025 20:48:33 +0000 Subject: [PATCH 01/12] Initial commit --- .gitignore | 33 +++++++ README.md | 96 ++----------------- pom.xml | 81 ++++++++++++++++ .../ActiveMQConfiguration.java | 19 ++++ .../osl/secretcontroller/Application.java | 31 ++++++ .../osl/secretcontroller/CatalogClient.java | 35 +++++++ .../secretcontroller/ServiceListenerBean.java | 89 +++++++++++++++++ .../secretcontroller/VaultConfiguration.java | 10 ++ .../secretcontroller/ApplicationTests.java | 13 +++ 9 files changed, 320 insertions(+), 87 deletions(-) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/Application.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java create mode 100644 src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +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/README.md b/README.md index 37eb62f..4615815 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,15 @@ -# org.etsi.osl.controllers.secrets +# Openslice secret controller +Generic controller for Openslice that automatically stores secret +characteristics into an external secret management solution and replaces the +original characteristic with a link to the external secret management solution. +## Status -## Getting started +Work in progress -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +## Vault setup -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: - -``` -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 +```sh +$ vault secrets enable -path openslice -version=2 -options=max_versions=1 kv ``` - -## 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. - -* [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/) - -*** - -# 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. - -## Suggestions for a good README - -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. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1436f5d --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + org.etsi.osl + secret-controller + 0.0.1-SNAPSHOT + org.etsi.osl.secret-controller + org.etsi.osl.secret-controller + + + + + + + + + + + + + + + + + 17 + 1.2.0 + + + + + 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.etsi.osl + org.etsi.osl.model.tmf + ${org.etsi.osl.model.tmf.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java new file mode 100644 index 0000000..00d89e6 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java @@ -0,0 +1,19 @@ +package org.etsi.osl.secretcontroller; + +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/Application.java b/src/main/java/org/etsi/osl/secretcontroller/Application.java new file mode 100644 index 0000000..e008ac9 --- /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 0000000..1aec5fb --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java @@ -0,0 +1,35 @@ +package org.etsi.osl.secretcontroller; + +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; + + @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); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java new file mode 100644 index 0000000..6a603b6 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java @@ -0,0 +1,89 @@ +package org.etsi.osl.secretcontroller; + +import org.etsi.osl.tmf.sim638.model.Service; +import org.etsi.osl.tmf.sim638.model.ServiceAttributeValueChangeNotification; +import org.etsi.osl.tmf.sim638.model.ServiceCreateNotification; +import org.etsi.osl.tmf.sim638.model.ServiceUpdate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; +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 tools.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +@Component +public class ServiceListenerBean { + private final String vaultBaseURI; + private final String vaultKVPath; + private final VaultKeyValueOperations vaultKV; + + private final CatalogClient catalogClient; + + private final ObjectMapper objectMapper; + + @Autowired + public ServiceListenerBean( + CatalogClient catalogClient, + VaultTemplate vaultTemplate, + ObjectMapper objectMapper, + @Value("${vault.uri}") String vaultUri, + @Value("${app.kvPath}") String kvPath + ) { + this.catalogClient = catalogClient; + this.objectMapper = objectMapper; + + this.vaultKV = vaultTemplate.opsForKeyValue(kvPath, VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); + this.vaultKVPath = kvPath; + if (vaultUri.endsWith("/")) + this.vaultBaseURI = vaultUri.substring(0, vaultUri.length() - 1); + else + this.vaultBaseURI = vaultUri; + } + + @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 serviceUpdate = new ServiceUpdate(); + + for (final var characteristic : svc.getServiceCharacteristic()) { + if (!characteristic.getName().equals("SecretCharacheteristic")) + continue; + + if (characteristic.getValue().getValue().startsWith(this.vaultBaseURI)) + continue; + + final var vaultKey = String.format("service::%s::%s", svc.getId(), characteristic.getName()); + final var secretData = Map.of("value", characteristic.getValue().getValue()); + this.vaultKV.put(vaultKey, Map.of("data", secretData)); + + final var secretURI = String.format( + "%s/v1/%s/data%s", + vaultBaseURI, + vaultKVPath, + UriUtils.encodePathSegment(vaultKey, StandardCharsets.US_ASCII) + ); + + characteristic.getValue().setValue(secretURI); + serviceUpdate.addServiceCharacteristicItem(characteristic); + } + + catalogClient.updateService(svc.getId(), serviceUpdate); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java new file mode 100644 index 0000000..45e23a3 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java @@ -0,0 +1,10 @@ +package org.etsi.osl.secretcontroller; + +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/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java new file mode 100644 index 0000000..578d659 --- /dev/null +++ b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java @@ -0,0 +1,13 @@ +package org.etsi.osl.secretcontroller; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } + +} -- GitLab From ee7b2a0bae4a57e0297885267f78b4695b19ac7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Tue, 9 Dec 2025 12:13:07 +0000 Subject: [PATCH 02/12] Implement cleanup --- .gitignore | 3 + .../secretcontroller/ServiceListenerBean.java | 62 ++++++++++++------- src/main/resources/application.properties | 13 ++++ 3 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/main/resources/application.properties diff --git a/.gitignore b/.gitignore index 667aaef..8767008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +application-prod.properties +application-dev.properties + HELP.md target/ .mvn/wrapper/maven-wrapper.jar diff --git a/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java index 6a603b6..366421f 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java @@ -1,9 +1,6 @@ package org.etsi.osl.secretcontroller; -import org.etsi.osl.tmf.sim638.model.Service; -import org.etsi.osl.tmf.sim638.model.ServiceAttributeValueChangeNotification; -import org.etsi.osl.tmf.sim638.model.ServiceCreateNotification; -import org.etsi.osl.tmf.sim638.model.ServiceUpdate; +import org.etsi.osl.tmf.sim638.model.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.jms.annotation.JmsListener; @@ -15,12 +12,12 @@ import org.springframework.web.util.UriUtils; import tools.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; @Component public class ServiceListenerBean { - private final String vaultBaseURI; - private final String vaultKVPath; + private final String vaultBaseSecretUri; private final VaultKeyValueOperations vaultKV; private final CatalogClient catalogClient; @@ -39,11 +36,23 @@ public class ServiceListenerBean { this.objectMapper = objectMapper; this.vaultKV = vaultTemplate.opsForKeyValue(kvPath, VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); - this.vaultKVPath = kvPath; + if (vaultUri.endsWith("/")) - this.vaultBaseURI = vaultUri.substring(0, vaultUri.length() - 1); - else - this.vaultBaseURI = vaultUri; + vaultUri = vaultUri.substring(0, vaultUri.length() - 1); + + this.vaultBaseSecretUri = String.format("%s/v1/%s", vaultUri, kvPath); + } + + protected final String getVaultServiceKey(String serviceId) { + return String.format("service::%s", serviceId); + } + + protected final String getVaultUri(String secretUri) { + return String.format( + "%s/data/%s", + vaultBaseSecretUri, + UriUtils.encodePathSegment(secretUri, StandardCharsets.US_ASCII) + ); } @JmsListener(destination = "${topics.EVENT_SERVICE_CREATE}", containerFactory = "topicJmsListenerContainerFactory") @@ -61,29 +70,40 @@ public class ServiceListenerBean { protected void processService(Service svc) { final var serviceUpdate = new ServiceUpdate(); + final var secretData = new HashMap(); + + final var vaultKey = getVaultServiceKey(svc.getId()); + final var secretURI = getVaultUri(vaultKey); for (final var characteristic : svc.getServiceCharacteristic()) { if (!characteristic.getName().equals("SecretCharacheteristic")) continue; - if (characteristic.getValue().getValue().startsWith(this.vaultBaseURI)) + if (characteristic.getValue().getValue().startsWith(this.vaultBaseSecretUri)) continue; - final var vaultKey = String.format("service::%s::%s", svc.getId(), characteristic.getName()); - final var secretData = Map.of("value", characteristic.getValue().getValue()); - this.vaultKV.put(vaultKey, Map.of("data", secretData)); - - final var secretURI = String.format( - "%s/v1/%s/data%s", - vaultBaseURI, - vaultKVPath, - UriUtils.encodePathSegment(vaultKey, StandardCharsets.US_ASCII) - ); + secretData.put(characteristic.getName(), characteristic.getValue().getValue()); characteristic.getValue().setValue(secretURI); serviceUpdate.addServiceCharacteristicItem(characteristic); } + if (secretData.isEmpty()) + return; + + this.vaultKV.put(vaultKey, Map.of("data", 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 = getVaultServiceKey(svc.getId()); + final var secretURI = getVaultUri(vaultKey); + + this.vaultKV.delete(secretURI); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..4388cdf --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.application.name=org.etsi.osl.secret-controller +spring.profiles.active=dev +app.kvPath=openslice +# Broker configuration +spring.artemis.mode=native +spring.artemis.broker-url=tcp://localhost:61616?jms.watchTopicAdvisories=false +# Broker topics +topics.CATALOG_UPD_SERVICE=CATALOG.UPD.SERVICE +topics.EVENT_SERVICE_CREATE=EVENT.SERVICE.CREATE +topics.EVENT_SERVICE_ATTRIBUTE_VALUE_CHANGED=EVENT.SERVICE.ATTRCHANGED +topics.EVENT_SERVICE_DELETE=EVENT.SERVICE.DELETE +# Vault configuration +vault.uri=http://localhost:8200 -- GitLab From dcb3f3f0d47588657354649ecd6726ef4ef50e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Tue, 9 Dec 2025 18:32:20 +0000 Subject: [PATCH 03/12] Implement resource secret support --- pom.xml | 4 + .../osl/secretcontroller/CatalogClient.java | 14 +++ .../ResourceRulesService.java | 114 ++++++++++++++++++ .../secretcontroller/SecretApplyResult.java | 35 ++++++ .../secretcontroller/ServiceRulesService.java | 57 +++++++++ .../osl/secretcontroller/VaultService.java | 63 ++++++++++ .../{ => config}/ActiveMQConfiguration.java | 2 +- .../secretcontroller/config/RulesConfig.java | 34 ++++++ .../{ => config}/VaultConfiguration.java | 2 +- .../listeners/ResourceListenerBean.java | 110 +++++++++++++++++ .../{ => listeners}/ServiceListenerBean.java | 52 ++++++-- .../rules/CharacteristicRule.java | 43 +++++++ .../secretcontroller/rules/ResourceRule.java | 32 +++++ .../rules/RuleEvaluatorService.java | 34 ++++++ .../secretcontroller/rules/ServiceRule.java | 6 + src/main/resources/application.properties | 13 -- src/main/resources/application.yml | 33 +++++ 17 files changed, 623 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/SecretApplyResult.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/VaultService.java rename src/main/java/org/etsi/osl/secretcontroller/{ => config}/ActiveMQConfiguration.java (93%) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/config/RulesConfig.java rename src/main/java/org/etsi/osl/secretcontroller/{ => config}/VaultConfiguration.java (86%) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java rename src/main/java/org/etsi/osl/secretcontroller/{ => listeners}/ServiceListenerBean.java (67%) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java create mode 100644 src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/pom.xml b/pom.xml index 1436f5d..130f821 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,10 @@ org.etsi.osl.model.tmf ${org.etsi.osl.model.tmf.version} + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java index 1aec5fb..1d619da 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java +++ b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java @@ -16,6 +16,9 @@ public class CatalogClient { @Value("${topics.CATALOG_UPD_SERVICE}") private String CATALOG_UPD_SERVICE; + @Value("${topics.CATALOG_UPD_RESOURCE}") + private String CATALOG_UPD_RESOURCE; + @Autowired public CatalogClient(JmsClient jmsClient, ObjectMapper objectMapper) { this.jmsClient = jmsClient; @@ -32,4 +35,15 @@ public class CatalogClient { 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); + } } 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 0000000..178f96c --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java @@ -0,0 +1,114 @@ +package org.etsi.osl.secretcontroller; + +import org.etsi.osl.secretcontroller.config.RulesConfig; +import org.etsi.osl.secretcontroller.rules.CharacteristicRule; +import org.etsi.osl.secretcontroller.rules.ResourceRule; +import org.etsi.osl.secretcontroller.rules.RuleEvaluatorService; +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.core.exc.StreamReadException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; +import java.util.Map; +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 Optional getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) { + final var cat = resource.getCategory(); + final var rule = this.rulesConfig.getResourceRules().get(cat); + + if (rule == null) + return Optional.empty(); + + return Optional.of(new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource, rule)); + } + + public static class ResourceRulesApplier { + private final ResourceRule rule; + private final RuleEvaluatorService ruleEvaluatorService; + + private final Map extraRules = new HashMap<>(); + + public ResourceRulesApplier(ObjectMapper mapper, + RuleEvaluatorService ruleEvaluatorService, + org.etsi.osl.tmf.ri639.model.Resource resource, + ResourceRule rule) { + this.rule = rule; + this.ruleEvaluatorService = ruleEvaluatorService; + + if (!rule.isKubernetesSecret()) + return; + + // Kubernetes secret extra handling + final var rawSecret = resource.getResourceCharacteristicByName("json").getValue().getValue(); + final JsonNode json; + try { + json = mapper.readTree(rawSecret); + } catch (StreamReadException e) { + // If reading fails the secret most likely has already been erased + log.debug(e.getMessage()); + return; + } + + final var secretData = json.get("data"); + // The secret may have already been erased + if (secretData == null || !secretData.isObject()) + return; + + final var replaceRule = new CharacteristicRule(); + replaceRule.setAction(SecretApplyResult.Action.Replace); + replaceRule.setBase64Decode(true); + + // Convert all characteristics with secret data into secrets + for (final var field : secretData.propertyNames()) + this.extraRules.put(field, replaceRule); + + final var eraseRule = new CharacteristicRule(); + eraseRule.setAction(SecretApplyResult.Action.Erase); + + // All these fields contain the secret data in one way or another and must be erased + this.extraRules.put("data", eraseRule); + this.extraRules.put("json", eraseRule); + this.extraRules.put("metadata", eraseRule); + } + + public SecretApplyResult saveCharacteristic(Characteristic characteristic) { + final var name = characteristic.getName(); + + final var extraRule = this.extraRules.get(name); + if (extraRule != null) + return this.ruleEvaluatorService.evaluateRule(characteristic.getValue().getValue(), extraRule); + + for (final var cRule : this.rule.getCharacteristics()) { + final var m = cRule.getMatch().matcher(name); + if (!m.matches()) + continue; + + final var value = characteristic.getValue().getValue(); + return this.ruleEvaluatorService.evaluateRule(value, cRule); + } + + 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 0000000..c4db983 --- /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 0000000..5c2139a --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java @@ -0,0 +1,57 @@ +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.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 Optional getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) { + final var name = svc.getName(); + final var rule = this.rulesConfig.getServiceRules().get(name); + + if (rule == null) + return Optional.empty(); + + return Optional.of(new ServiceRulesApplier(this.ruleEvaluatorService, rule)); + } + + public static class ServiceRulesApplier { + private final ServiceRule rule; + private final RuleEvaluatorService ruleEvaluatorService; + + public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, ServiceRule rule) { + this.rule = rule; + this.ruleEvaluatorService = ruleEvaluatorService; + } + + public SecretApplyResult saveCharacteristic(Characteristic characteristic) { + final var name = characteristic.getName(); + + for (final var cRule : this.rule.characteristics()) { + final var m = cRule.getMatch().matcher(name); + if (!m.matches()) + continue; + + var value = characteristic.getValue().getValue(); + return this.ruleEvaluatorService.evaluateRule(value, cRule); + } + + 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 0000000..1cd0769 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java @@ -0,0 +1,63 @@ +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 tools.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +@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", vaultUri, kvPath); + } + + public final String getVaultKey(String namespace, String key) { + return String.format("%s::%s", namespace, key); + } + + public final String getVaultUri(String secretKey) { + return String.format( + "%s/data/%s", + vaultBaseSecretUri, + UriUtils.encodePathSegment(secretKey, StandardCharsets.US_ASCII) + ); + } + + public boolean isAlreadySecret(String value) { + return value.startsWith(this.vaultBaseSecretUri); + } + + public void upsertSecret(String key, Map data) { + final var secret = Map.of("data", data); + try { + this.vaultKV.patch(key, secret); + } catch (SecretNotFoundException e) { + this.vaultKV.put(key, secret); + } + } + + public void deleteSecret(String key) { + this.vaultKV.delete(key); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java similarity index 93% rename from src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java rename to src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java index 00d89e6..0290cde 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ActiveMQConfiguration.java +++ b/src/main/java/org/etsi/osl/secretcontroller/config/ActiveMQConfiguration.java @@ -1,4 +1,4 @@ -package org.etsi.osl.secretcontroller; +package org.etsi.osl.secretcontroller.config; import jakarta.jms.ConnectionFactory; import org.springframework.context.annotation.Bean; 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 0000000..50113fb --- /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/VaultConfiguration.java b/src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java similarity index 86% rename from src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java rename to src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java index 45e23a3..d87811f 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/VaultConfiguration.java +++ b/src/main/java/org/etsi/osl/secretcontroller/config/VaultConfiguration.java @@ -1,4 +1,4 @@ -package org.etsi.osl.secretcontroller; +package org.etsi.osl.secretcontroller.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java new file mode 100644 index 0000000..3259c49 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java @@ -0,0 +1,110 @@ +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 ResourceListenerBean { + 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 ResourceListenerBean( + 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 ruleApplierOpt = this.resourceRulesService.getResourceRules(resource); + if (ruleApplierOpt.isEmpty()) + return; + + final var ruleApplier = ruleApplierOpt.get(); + + 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); + } + +// TODO: Resolve upstream problems +// @JmsListener(destination = "${topics.EVENT_RESOURCE_DELETE}", containerFactory = +// "topicJmsListenerContainerFactory") +// public void processResourceDelete(String content) { +// final var res = this.objectMapper.readValue(content, ResourceDelete.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/ServiceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java similarity index 67% rename from src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java rename to src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java index 366421f..86252cf 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ServiceListenerBean.java +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java @@ -1,10 +1,14 @@ -package org.etsi.osl.secretcontroller; +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.tmf.sim638.model.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; +import org.springframework.vault.core.SecretNotFoundException; import org.springframework.vault.core.VaultKeyValueOperations; import org.springframework.vault.core.VaultKeyValueOperationsSupport; import org.springframework.vault.core.VaultTemplate; @@ -21,18 +25,21 @@ public class ServiceListenerBean { private final VaultKeyValueOperations vaultKV; private final CatalogClient catalogClient; + private final ServiceRulesService serviceRulesService; private final ObjectMapper objectMapper; @Autowired public ServiceListenerBean( CatalogClient catalogClient, + ServiceRulesService serviceRulesService, VaultTemplate vaultTemplate, ObjectMapper objectMapper, @Value("${vault.uri}") String vaultUri, - @Value("${app.kvPath}") String kvPath + @Value("${vault.kvPath}") String kvPath ) { this.catalogClient = catalogClient; + this.serviceRulesService = serviceRulesService; this.objectMapper = objectMapper; this.vaultKV = vaultTemplate.opsForKeyValue(kvPath, VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); @@ -69,29 +76,54 @@ public class ServiceListenerBean { } protected void processService(Service svc) { + final var ruleApplierOpt = this.serviceRulesService.getServiceRules(svc); + if (ruleApplierOpt.isEmpty()) + return; + + final var ruleApplier = ruleApplierOpt.get(); + final var serviceUpdate = new ServiceUpdate(); final var secretData = new HashMap(); final var vaultKey = getVaultServiceKey(svc.getId()); final var secretURI = getVaultUri(vaultKey); + var dirty = false; for (final var characteristic : svc.getServiceCharacteristic()) { - if (!characteristic.getName().equals("SecretCharacheteristic")) - continue; - + // Characteristic was already stored, ignore it if (characteristic.getValue().getValue().startsWith(this.vaultBaseSecretUri)) continue; - secretData.put(characteristic.getName(), characteristic.getValue().getValue()); - - characteristic.getValue().setValue(secretURI); - serviceUpdate.addServiceCharacteristicItem(characteristic); + 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()) return; - this.vaultKV.put(vaultKey, Map.of("data", secretData)); + final var secret = Map.of("data", secretData); + try { + this.vaultKV.patch(vaultKey, secret); + } catch (SecretNotFoundException e) { + this.vaultKV.put(vaultKey, secret); + } + catalogClient.updateService(svc.getId(), serviceUpdate); } 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 0000000..9f2cd5f --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java @@ -0,0 +1,43 @@ +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; + private boolean base64Decode = false; + + 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; + } + + public boolean isBase64Decode() { + return base64Decode; + } + + public void setBase64Decode(boolean base64Decode) { + this.base64Decode = base64Decode; + } + + @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 0000000..48267c0 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java @@ -0,0 +1,32 @@ +package org.etsi.osl.secretcontroller.rules; + +import java.util.List; + +public final class ResourceRule { + private boolean kubernetesSecret = false; + private List characteristics = List.of(); + + public boolean isKubernetesSecret() { + return kubernetesSecret; + } + + public void setKubernetesSecret(boolean kubernetesSecret) { + this.kubernetesSecret = kubernetesSecret; + } + + public List getCharacteristics() { + return characteristics; + } + + public void setCharacteristics(List characteristics) { + this.characteristics = characteristics; + } + + @Override + public String toString() { + return "ResourceRule[" + + "kubernetesSecret=" + kubernetesSecret + + ", 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 0000000..d35d824 --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java @@ -0,0 +1,34 @@ +package org.etsi.osl.secretcontroller.rules; + +import org.etsi.osl.secretcontroller.SecretApplyResult; +import org.springframework.stereotype.Service; + +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Service +public class RuleEvaluatorService { + public final SecretApplyResult evaluateRule(String value, CharacteristicRule rule) { + return switch (rule.getAction()) { + case Replace -> { + if (rule.isBase64Decode()) { + try { + final var rawBytes = Base64.getDecoder().decode(value); + final var decoder = StandardCharsets.UTF_8.newDecoder(); + final var byteBuffer = ByteBuffer.wrap(rawBytes); + final var charBuffer = decoder.decode(byteBuffer); + value = charBuffer.toString(); + } catch (CharacterCodingException | IllegalArgumentException e) { + // Ignore decoding errors (the value might not be a string) and use the base64 values + } + } + + yield 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 0000000..f689bbf --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java @@ -0,0 +1,6 @@ +package org.etsi.osl.secretcontroller.rules; + +import java.util.List; + +public record ServiceRule(List characteristics) { +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 4388cdf..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,13 +0,0 @@ -spring.application.name=org.etsi.osl.secret-controller -spring.profiles.active=dev -app.kvPath=openslice -# Broker configuration -spring.artemis.mode=native -spring.artemis.broker-url=tcp://localhost:61616?jms.watchTopicAdvisories=false -# Broker topics -topics.CATALOG_UPD_SERVICE=CATALOG.UPD.SERVICE -topics.EVENT_SERVICE_CREATE=EVENT.SERVICE.CREATE -topics.EVENT_SERVICE_ATTRIBUTE_VALUE_CHANGED=EVENT.SERVICE.ATTRCHANGED -topics.EVENT_SERVICE_DELETE=EVENT.SERVICE.DELETE -# Vault configuration -vault.uri=http://localhost:8200 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..b82a351 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,33 @@ +spring: + application: + name: "org.etsi.osl.secret-controller" + profiles: + active: "dev" + + artemis: + mode: "native" + broker-url: "tcp://localhost:61616?jms.watchTopicAdvisories=false" + +vault: + uri: "http://localhost:8200" + kvPath: "openslice" + +topics: + 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" + +app: + service-rules: + "ServiceWithSecret": + characteristics: + - match: "SecretCharacheteristic" + resource-rules: + "[Secret/Kubernetes/v1]": + kubernetes-secret: true \ No newline at end of file -- GitLab From 2d4da1ccccc0cf6b0a0d020ba282006196abdfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Mon, 15 Dec 2025 15:21:08 +0000 Subject: [PATCH 04/12] Seal secrets based on characteristic value type --- pom.xml | 2 +- .../ResourceRulesService.java | 85 +++++-------------- .../secretcontroller/ServiceRulesService.java | 34 ++++---- .../osl/secretcontroller/VaultService.java | 39 ++++++--- .../listeners/ControllerCommandsListener.java | 67 +++++++++++++++ ...rBean.java => ResourceEventsListener.java} | 10 +-- ...erBean.java => ServiceEventsListener.java} | 73 ++++------------ .../commands/UnsealCharacteristicCommand.java | 4 + .../rules/CharacteristicRule.java | 9 -- .../secretcontroller/rules/ResourceRule.java | 12 +-- .../rules/RuleEvaluatorService.java | 21 +---- .../secretcontroller/rules/ServiceRule.java | 18 +++- src/main/resources/application.yml | 11 +-- 13 files changed, 182 insertions(+), 203 deletions(-) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java rename src/main/java/org/etsi/osl/secretcontroller/listeners/{ResourceListenerBean.java => ResourceEventsListener.java} (94%) rename src/main/java/org/etsi/osl/secretcontroller/listeners/{ServiceListenerBean.java => ServiceEventsListener.java} (58%) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/listeners/commands/UnsealCharacteristicCommand.java diff --git a/pom.xml b/pom.xml index 130f821..bc688e3 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 17 - 1.2.0 + 1.3.0-alpha diff --git a/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java b/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java index 178f96c..7eec9a0 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/ResourceRulesService.java @@ -1,20 +1,16 @@ package org.etsi.osl.secretcontroller; import org.etsi.osl.secretcontroller.config.RulesConfig; -import org.etsi.osl.secretcontroller.rules.CharacteristicRule; 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.core.exc.StreamReadException; -import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; @Service @@ -33,79 +29,44 @@ public class ResourceRulesService { this.mapper = mapper; } - public Optional getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) { - final var cat = resource.getCategory(); - final var rule = this.rulesConfig.getResourceRules().get(cat); + public ResourceRulesApplier getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) { + ResourceRule rule = null; - if (rule == null) - return Optional.empty(); + final var cat = resource.getCategory(); + if (cat != null) + rule = this.rulesConfig.getResourceRules().get(cat); - return Optional.of(new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource, rule)); + return new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource, + Optional.ofNullable(rule)); } public static class ResourceRulesApplier { - private final ResourceRule rule; + private final Optional rule; private final RuleEvaluatorService ruleEvaluatorService; - private final Map extraRules = new HashMap<>(); - public ResourceRulesApplier(ObjectMapper mapper, RuleEvaluatorService ruleEvaluatorService, org.etsi.osl.tmf.ri639.model.Resource resource, - ResourceRule rule) { + Optional rule) { this.rule = rule; this.ruleEvaluatorService = ruleEvaluatorService; - - if (!rule.isKubernetesSecret()) - return; - - // Kubernetes secret extra handling - final var rawSecret = resource.getResourceCharacteristicByName("json").getValue().getValue(); - final JsonNode json; - try { - json = mapper.readTree(rawSecret); - } catch (StreamReadException e) { - // If reading fails the secret most likely has already been erased - log.debug(e.getMessage()); - return; - } - - final var secretData = json.get("data"); - // The secret may have already been erased - if (secretData == null || !secretData.isObject()) - return; - - final var replaceRule = new CharacteristicRule(); - replaceRule.setAction(SecretApplyResult.Action.Replace); - replaceRule.setBase64Decode(true); - - // Convert all characteristics with secret data into secrets - for (final var field : secretData.propertyNames()) - this.extraRules.put(field, replaceRule); - - final var eraseRule = new CharacteristicRule(); - eraseRule.setAction(SecretApplyResult.Action.Erase); - - // All these fields contain the secret data in one way or another and must be erased - this.extraRules.put("data", eraseRule); - this.extraRules.put("json", eraseRule); - this.extraRules.put("metadata", eraseRule); } public SecretApplyResult saveCharacteristic(Characteristic characteristic) { final var name = characteristic.getName(); - - final var extraRule = this.extraRules.get(name); - if (extraRule != null) - return this.ruleEvaluatorService.evaluateRule(characteristic.getValue().getValue(), extraRule); - - for (final var cRule : this.rule.getCharacteristics()) { - final var m = cRule.getMatch().matcher(name); - if (!m.matches()) - continue; - - final var value = characteristic.getValue().getValue(); - return this.ruleEvaluatorService.evaluateRule(value, cRule); + 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/ServiceRulesService.java b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java index 5c2139a..1a15d55 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java @@ -3,6 +3,7 @@ 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; @@ -20,35 +21,36 @@ public class ServiceRulesService { this.ruleEvaluatorService = ruleEvaluatorService; } - public Optional getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) { + public ServiceRulesApplier getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) { final var name = svc.getName(); final var rule = this.rulesConfig.getServiceRules().get(name); - - if (rule == null) - return Optional.empty(); - - return Optional.of(new ServiceRulesApplier(this.ruleEvaluatorService, rule)); + return new ServiceRulesApplier(this.ruleEvaluatorService, Optional.ofNullable(rule)); } public static class ServiceRulesApplier { - private final ServiceRule rule; + private final Optional rule; private final RuleEvaluatorService ruleEvaluatorService; - public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, ServiceRule rule) { + public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, Optional rule) { this.rule = rule; this.ruleEvaluatorService = ruleEvaluatorService; } public SecretApplyResult saveCharacteristic(Characteristic characteristic) { final var name = characteristic.getName(); - - for (final var cRule : this.rule.characteristics()) { - final var m = cRule.getMatch().matcher(name); - if (!m.matches()) - continue; - - var value = characteristic.getValue().getValue(); - return this.ruleEvaluatorService.evaluateRule(value, cRule); + 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 index 1cd0769..1384f67 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/VaultService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java @@ -8,10 +8,10 @@ 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 tools.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Optional; @Service public class VaultService { @@ -29,31 +29,44 @@ public class VaultService { if (vaultUri.endsWith("/")) vaultUri = vaultUri.substring(0, vaultUri.length() - 1); - this.vaultBaseSecretUri = String.format("%s/v1/%s", vaultUri, kvPath); + this.vaultBaseSecretUri = String.format("%s/v1/%s/data/", vaultUri, kvPath); } - public final String getVaultKey(String namespace, String key) { - return String.format("%s::%s", namespace, key); + public String getVaultKey(String namespace, String key) { + return String.format("%s/%s", namespace, key); } - public final String getVaultUri(String secretKey) { - return String.format( - "%s/data/%s", - vaultBaseSecretUri, - UriUtils.encodePathSegment(secretKey, StandardCharsets.US_ASCII) - ); + 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) { - final var secret = Map.of("data", data); try { - this.vaultKV.patch(key, secret); + this.vaultKV.patch(key, data); } catch (SecretNotFoundException e) { - this.vaultKV.put(key, secret); + this.vaultKV.put(key, data); } } 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 0000000..f1ad4bd --- /dev/null +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java @@ -0,0 +1,67 @@ +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.Map; + +@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"); + } + + this.jmsClient.destination(destination) + .send(this.objectMapper.writeValueAsString(result), + Map.of(JmsHeaders.CORRELATION_ID, jmsHeaderAccessor.getCorrelationId())); + } +} diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java similarity index 94% rename from src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java rename to src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java index 3259c49..41a814e 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceListenerBean.java +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java @@ -16,7 +16,7 @@ import tools.jackson.databind.ObjectMapper; import java.util.HashMap; @Component -public class ResourceListenerBean { +public class ResourceEventsListener { private static final String SECRET_NAMESPACE = "resource"; private final VaultService vaultService; @@ -27,7 +27,7 @@ public class ResourceListenerBean { private final ObjectMapper objectMapper; @Autowired - public ResourceListenerBean( + public ResourceEventsListener( CatalogClient catalogClient, ResourceRulesService resourceRulesService, VaultService vaultService, @@ -53,11 +53,7 @@ public class ResourceListenerBean { } protected void processResource(Resource resource) { - final var ruleApplierOpt = this.resourceRulesService.getResourceRules(resource); - if (ruleApplierOpt.isEmpty()) - return; - - final var ruleApplier = ruleApplierOpt.get(); + final var ruleApplier = this.resourceRulesService.getResourceRules(resource); final var resourceUpdate = new ResourceUpdate(); final var secretData = new HashMap(); diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java similarity index 58% rename from src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java rename to src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java index 86252cf..0f65c0c 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceListenerBean.java +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceEventsListener.java @@ -3,26 +3,20 @@ 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.beans.factory.annotation.Value; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; -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 tools.jackson.databind.ObjectMapper; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.Map; @Component -public class ServiceListenerBean { - private final String vaultBaseSecretUri; - private final VaultKeyValueOperations vaultKV; +public class ServiceEventsListener { + private static final String SECRET_NAMESPACE = "service"; + + private final VaultService vaultService; private final CatalogClient catalogClient; private final ServiceRulesService serviceRulesService; @@ -30,36 +24,16 @@ public class ServiceListenerBean { private final ObjectMapper objectMapper; @Autowired - public ServiceListenerBean( + public ServiceEventsListener( + VaultService vaultService, CatalogClient catalogClient, ServiceRulesService serviceRulesService, - VaultTemplate vaultTemplate, - ObjectMapper objectMapper, - @Value("${vault.uri}") String vaultUri, - @Value("${vault.kvPath}") String kvPath + ObjectMapper objectMapper ) { + this.vaultService = vaultService; this.catalogClient = catalogClient; this.serviceRulesService = serviceRulesService; this.objectMapper = objectMapper; - - 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", vaultUri, kvPath); - } - - protected final String getVaultServiceKey(String serviceId) { - return String.format("service::%s", serviceId); - } - - protected final String getVaultUri(String secretUri) { - return String.format( - "%s/data/%s", - vaultBaseSecretUri, - UriUtils.encodePathSegment(secretUri, StandardCharsets.US_ASCII) - ); } @JmsListener(destination = "${topics.EVENT_SERVICE_CREATE}", containerFactory = "topicJmsListenerContainerFactory") @@ -76,22 +50,18 @@ public class ServiceListenerBean { } protected void processService(Service svc) { - final var ruleApplierOpt = this.serviceRulesService.getServiceRules(svc); - if (ruleApplierOpt.isEmpty()) - return; - - final var ruleApplier = ruleApplierOpt.get(); + final var ruleApplier = this.serviceRulesService.getServiceRules(svc); final var serviceUpdate = new ServiceUpdate(); final var secretData = new HashMap(); - final var vaultKey = getVaultServiceKey(svc.getId()); - final var secretURI = getVaultUri(vaultKey); + 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 (characteristic.getValue().getValue().startsWith(this.vaultBaseSecretUri)) + if (this.vaultService.isAlreadySecret(characteristic.getValue().getValue())) continue; final var storeSecret = ruleApplier.saveCharacteristic(characteristic); @@ -114,15 +84,8 @@ public class ServiceListenerBean { if (!dirty) return; - if (secretData.isEmpty()) - return; - - final var secret = Map.of("data", secretData); - try { - this.vaultKV.patch(vaultKey, secret); - } catch (SecretNotFoundException e) { - this.vaultKV.put(vaultKey, secret); - } + if (!secretData.isEmpty()) + this.vaultService.upsertSecret(vaultKey, secretData); catalogClient.updateService(svc.getId(), serviceUpdate); } @@ -133,9 +96,7 @@ public class ServiceListenerBean { final var res = this.objectMapper.readValue(content, ServiceDeleteNotification.class); final var svc = res.getEvent().getService(); - final var vaultKey = getVaultServiceKey(svc.getId()); - final var secretURI = getVaultUri(vaultKey); - - this.vaultKV.delete(secretURI); + 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/commands/UnsealCharacteristicCommand.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/commands/UnsealCharacteristicCommand.java new file mode 100644 index 0000000..14ec048 --- /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 index 9f2cd5f..f97a236 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/CharacteristicRule.java @@ -7,7 +7,6 @@ import java.util.regex.Pattern; public final class CharacteristicRule { private Pattern match; private SecretApplyResult.Action action = SecretApplyResult.Action.Replace; - private boolean base64Decode = false; public Pattern getMatch() { return match; @@ -25,14 +24,6 @@ public final class CharacteristicRule { this.action = action; } - public boolean isBase64Decode() { - return base64Decode; - } - - public void setBase64Decode(boolean base64Decode) { - this.base64Decode = base64Decode; - } - @Override public String toString() { return "CharacteristicRule[" + diff --git a/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java b/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java index 48267c0..c19e226 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ResourceRule.java @@ -3,17 +3,8 @@ package org.etsi.osl.secretcontroller.rules; import java.util.List; public final class ResourceRule { - private boolean kubernetesSecret = false; private List characteristics = List.of(); - public boolean isKubernetesSecret() { - return kubernetesSecret; - } - - public void setKubernetesSecret(boolean kubernetesSecret) { - this.kubernetesSecret = kubernetesSecret; - } - public List getCharacteristics() { return characteristics; } @@ -25,8 +16,7 @@ public final class ResourceRule { @Override public String toString() { return "ResourceRule[" + - "kubernetesSecret=" + kubernetesSecret + - ", characteristics=" + characteristics + + "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 index d35d824..f034c6a 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/RuleEvaluatorService.java @@ -3,30 +3,11 @@ package org.etsi.osl.secretcontroller.rules; import org.etsi.osl.secretcontroller.SecretApplyResult; import org.springframework.stereotype.Service; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - @Service public class RuleEvaluatorService { public final SecretApplyResult evaluateRule(String value, CharacteristicRule rule) { return switch (rule.getAction()) { - case Replace -> { - if (rule.isBase64Decode()) { - try { - final var rawBytes = Base64.getDecoder().decode(value); - final var decoder = StandardCharsets.UTF_8.newDecoder(); - final var byteBuffer = ByteBuffer.wrap(rawBytes); - final var charBuffer = decoder.decode(byteBuffer); - value = charBuffer.toString(); - } catch (CharacterCodingException | IllegalArgumentException e) { - // Ignore decoding errors (the value might not be a string) and use the base64 values - } - } - - yield SecretApplyResult.replace(value); - } + 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 index f689bbf..0094a39 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java +++ b/src/main/java/org/etsi/osl/secretcontroller/rules/ServiceRule.java @@ -2,5 +2,21 @@ package org.etsi.osl.secretcontroller.rules; import java.util.List; -public record ServiceRule(List characteristics) { +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 index b82a351..c0d68e9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,8 @@ vault: 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" @@ -24,10 +26,5 @@ topics: EVENT_RESOURCE_DELETE: "EVENT.RESOURCE.DELETE" app: - service-rules: - "ServiceWithSecret": - characteristics: - - match: "SecretCharacheteristic" - resource-rules: - "[Secret/Kubernetes/v1]": - kubernetes-secret: true \ No newline at end of file + service-rules: { } + resource-rules: { } -- GitLab From 0ef5210d9d6de597625623a3e9a20339226aff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Wed, 17 Dec 2025 11:03:10 +0000 Subject: [PATCH 05/12] Seal service order order items secrets --- .../osl/secretcontroller/CatalogClient.java | 14 ++ .../secretcontroller/ServiceRulesService.java | 6 + .../osl/secretcontroller/VaultService.java | 4 +- .../listeners/ServiceOrderEventsListener.java | 122 ++++++++++++++++++ src/main/resources/application.yml | 5 + 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/etsi/osl/secretcontroller/listeners/ServiceOrderEventsListener.java diff --git a/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java index 1d619da..c5fbfcb 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java +++ b/src/main/java/org/etsi/osl/secretcontroller/CatalogClient.java @@ -1,5 +1,6 @@ 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; @@ -19,6 +20,9 @@ public class CatalogClient { @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; @@ -46,4 +50,14 @@ public class CatalogClient { 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/ServiceRulesService.java b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java index 1a15d55..e328e44 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/ServiceRulesService.java @@ -27,6 +27,12 @@ public class ServiceRulesService { 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; diff --git a/src/main/java/org/etsi/osl/secretcontroller/VaultService.java b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java index 1384f67..12a7535 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/VaultService.java +++ b/src/main/java/org/etsi/osl/secretcontroller/VaultService.java @@ -32,8 +32,8 @@ public class VaultService { this.vaultBaseSecretUri = String.format("%s/v1/%s/data/", vaultUri, kvPath); } - public String getVaultKey(String namespace, String key) { - return String.format("%s/%s", namespace, key); + public String getVaultKey(String namespace, String... keys) { + return String.format("%s/%s", namespace, String.join("/", keys)); } public String getVaultUri(String secretKey) { 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 0000000..ee11654 --- /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/resources/application.yml b/src/main/resources/application.yml index c0d68e9..d2fe647 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,6 +25,11 @@ topics: 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: { } -- GitLab From c9fa6983d3d0078319cf3f605cf48abbd9f92840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Thu, 12 Mar 2026 14:59:14 +0000 Subject: [PATCH 06/12] Documentation and compose --- Dockerfile | 4 +++ README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++--- compose.yml | 52 ++++++++++++++++++++++++++++++ pom.xml | 6 ++-- 4 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 Dockerfile create mode 100644 compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6a9b74 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +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 4615815..6524599 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,98 @@ # Openslice secret controller -Generic controller for Openslice that automatically stores secret -characteristics into an external secret management solution and replaces the -original characteristic with a link to the external secret management solution. +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. -## Status +## Configuration -Work in progress +The controller can be configured using the usual mechanisms provided by the +Spring framework, with the main options being the following: + +```yaml +spring: + artemis: + broker-url: "tcp://artemis:61616" + user: artemis + password: password + + +vault: + uri: "http://vault:8200" + kvPath: "openslice" + token: "hvs.AAAAAAAAAA" +``` + +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. ## 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: + ```sh $ vault secrets enable -path openslice -version=2 -options=max_versions=1 kv ``` + +Then a policy and token can be created for the controller using the following +commands: + +```sh +$ vault policy write openslice-policy - <org.etsi.osl secret-controller - 0.0.1-SNAPSHOT + 0.1.0 org.etsi.osl.secret-controller org.etsi.osl.secret-controller @@ -31,7 +31,9 @@ 17 - 1.3.0-alpha + 17 + 17 + 1.4.0-SNAPSHOT -- GitLab From 07f918845af14eef0f9b5df2fbba027ef5e836a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Thu, 2 Jul 2026 17:01:19 +0100 Subject: [PATCH 07/12] Remove dead code --- .../listeners/ResourceEventsListener.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java index 41a814e..64b0c27 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ResourceEventsListener.java @@ -92,15 +92,4 @@ public class ResourceEventsListener { catalogClient.updateResource(resource.getId(), resourceUpdate); } - -// TODO: Resolve upstream problems -// @JmsListener(destination = "${topics.EVENT_RESOURCE_DELETE}", containerFactory = -// "topicJmsListenerContainerFactory") -// public void processResourceDelete(String content) { -// final var res = this.objectMapper.readValue(content, ResourceDelete.class); -// final var svc = res.getEvent().getService(); -// -// final var vaultKey = this.vaultService.getVaultKey(SECRET_NAMESPACE, svc.getId()); -// this.vaultService.deleteSecret(vaultKey); -// } } -- GitLab From 7e7537d534c46deb6c6bf63cf44636085371d693 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Wed, 22 Jul 2026 14:00:50 +0300 Subject: [PATCH 08/12] fix for #1: Adding CI configuration and fixing docker-compose for non-dev deployments --- .gitlab-ci.yml | 40 +++++++++++++++++++++++++++++++++++ Dockerfile | 2 ++ ci_settings.xml | 16 ++++++++++++++ compose.yml | 56 ++++++++++++++++++++++++++++++++----------------- 4 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 ci_settings.xml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..40be386 --- /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 index c6a9b74..93889a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +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/ci_settings.xml b/ci_settings.xml new file mode 100644 index 0000000..69ad06e --- /dev/null +++ b/ci_settings.xml @@ -0,0 +1,16 @@ + + + + gitlab-maven + + + + Job-Token + ${CI_JOB_TOKEN} + + + + + + diff --git a/compose.yml b/compose.yml index 9986d52..eee85b3 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,33 @@ 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: - image: hashicorp/vault:2.0 container_name: osl-vault + image: hashicorp/vault:2.0 + restart: always + profiles: ["dev"] command: server ports: - "8200:8200" @@ -12,24 +38,13 @@ services: configs: - source: vault_config target: /vault/config/config.hcl - - osl-secrets-controller: - build: - context: . - dockerfile: Dockerfile - container_name: osl-secrets-controller - environment: - SPRING_ARTEMIS_BROKER_URL: tcp://artemis:61616 - SPRING_ARTEMIS_USER: artemis - SPRING_ARTEMIS_PASSWORD: password - VAULT_URI: http://osl-vault:8200 - VAULT_TOKEN: hvs.AAAAAAAAAAAA - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 10s + logging: + driver: "json-file" + options: + max-size: "200m" + max-file: "3" + networks: + - compose_back volumes: vault_data: @@ -50,3 +65,6 @@ configs: tls_disable = true } +networks: + compose_back: + external: true \ No newline at end of file -- GitLab From 59f823f177eb26adaf930259f58a36e60a86f81c Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Wed, 22 Jul 2026 17:36:35 +0300 Subject: [PATCH 09/12] fix test pipeline --- src/main/resources/application.yml | 2 -- src/test/resources/application.yml | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d2fe647..25ae261 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,6 @@ spring: application: name: "org.etsi.osl.secret-controller" - profiles: - active: "dev" artemis: mode: "native" diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..5d18c74 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + artemis: + username: "artemis" + password: "artemis" + +vault: + token: "hvs.AAAAAAAAAAAAA" + \ No newline at end of file -- GitLab From 1b51dccab2506ee38318dbc2e4c5733bb17ca71a Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Wed, 22 Jul 2026 17:48:41 +0300 Subject: [PATCH 10/12] fix for testing pipeline --- .../java/org/etsi/osl/secretcontroller/ApplicationTests.java | 2 ++ src/test/resources/{application.yml => application-test.yml} | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) rename src/test/resources/{application.yml => application-test.yml} (77%) diff --git a/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java index 578d659..e797aa8 100644 --- a/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java +++ b/src/test/java/org/etsi/osl/secretcontroller/ApplicationTests.java @@ -2,8 +2,10 @@ 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 diff --git a/src/test/resources/application.yml b/src/test/resources/application-test.yml similarity index 77% rename from src/test/resources/application.yml rename to src/test/resources/application-test.yml index 5d18c74..704003e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application-test.yml @@ -1,6 +1,6 @@ spring: artemis: - username: "artemis" + user: "artemis" password: "artemis" vault: -- GitLab From c03eb01d8554d2a31bf7e53185d69453022ec226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Capucho?= Date: Wed, 22 Jul 2026 17:26:27 +0100 Subject: [PATCH 11/12] Add tests for unseal listener --- pom.xml | 5 + .../listeners/ControllerCommandsListener.java | 10 +- .../ControllerCommandsListenerTest.java | 120 ++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListenerTest.java diff --git a/pom.xml b/pom.xml index 7189e06..e9a0d6a 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,11 @@ spring-boot-starter-artemis-test test + + org.apache.activemq + artemis-jms-server + test + org.etsi.osl diff --git a/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java b/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java index f1ad4bd..e1ab2f3 100644 --- a/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java +++ b/src/main/java/org/etsi/osl/secretcontroller/listeners/ControllerCommandsListener.java @@ -12,7 +12,7 @@ import org.springframework.jms.support.JmsMessageHeaderAccessor; import org.springframework.stereotype.Component; import tools.jackson.databind.ObjectMapper; -import java.util.Map; +import java.util.HashMap; @Component public class ControllerCommandsListener { @@ -60,8 +60,12 @@ public class ControllerCommandsListener { 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), - Map.of(JmsHeaders.CORRELATION_ID, jmsHeaderAccessor.getCorrelationId())); + .send(this.objectMapper.writeValueAsString(result), headers); } } 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 0000000..b6e844d --- /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()); + } + } + +} -- GitLab From b07a7c69b0fe565db305f6966ed2b1c5c704b7ab Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Thu, 23 Jul 2026 00:07:18 +0300 Subject: [PATCH 12/12] add startup banner --- src/main/resources/banner.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/resources/banner.txt diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..74229cd --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,11 @@ + ___ ____ _ _ + / _ \ _ __ ___ _ __ / ___|| (_) ___ ___ + | | | | '_ \ / _ \ '_ \\___ \| | |/ __/ _ \ + | |_| | |_) | __/ | | |___) | | | (_| __/ + \___/| .__/ \___|_| |_|____/|_|_|\___\___| + |_| + __ __________________ + / / __ __ / __/_ __/ __/ _/ + / _ \/ // / / _/ / / _\ \_/ / + /_.__/\_, / /___/ /_/ /___/___/ + /___/ \ No newline at end of file -- GitLab