You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by as...@apache.org on 2026/06/07 13:05:41 UTC
(ofbiz-plugins) branch trunk updated: Revert "Adding all the newly created Secret Manager Plugins. Currently testing is being done with AWS Secret Manager plugin. For other plugins, I need to create account at all the providers then will update the code if required."
This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-plugins.git
The following commit(s) were added to refs/heads/trunk by this push:
new 112fbeab6 Revert "Adding all the newly created Secret Manager Plugins. Currently testing is being done with AWS Secret Manager plugin. For other plugins, I need to create account at all the providers then will update the code if required."
112fbeab6 is described below
commit 112fbeab61f23bc5af2daa039d42f072c295ff19
Author: Ashish Vijaywargiya <as...@apache.org>
AuthorDate: Sun Jun 7 18:35:21 2026 +0530
Revert "Adding all the newly created Secret Manager Plugins. Currently testing is being done with AWS Secret Manager plugin. For other plugins, I need to create account at all the providers then will update the code if required."
This reverts commit a04675615cd9b7e19ae24970981301c03d6d2b6c.
---
aws-secrets-provider/build.gradle | 30 --
.../config/aws-secrets-manager.properties | 53 ---
aws-secrets-provider/ofbiz-component.xml | 30 --
.../awssecrets/AwsSecretsManagerProvider.java | 207 ---------
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../awssecrets/AwsSecretsManagerProviderTest.java | 163 --------
azure-keyvault-secrets-provider/build.gradle | 29 --
.../config/azure-keyvault.properties | 39 --
.../ofbiz-component.xml | 30 --
.../ofbiz/azurekeyvault/AzureKeyVaultReader.java | 35 --
.../AzureKeyVaultSecretsProvider.java | 180 --------
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../AzureKeyVaultSecretsProviderTest.java | 131 ------
bitwarden-secrets-provider/build.gradle | 27 --
.../config/bitwarden-secrets.properties | 59 ---
bitwarden-secrets-provider/ofbiz-component.xml | 30 --
.../ofbiz/bitwarden/BitwardenHttpClient.java | 49 ---
.../ofbiz/bitwarden/BitwardenSecretsProvider.java | 461 ---------------------
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../bitwarden/BitwardenSecretsProviderTest.java | 220 ----------
gcp-secretmanager-secrets-provider/build.gradle | 28 --
.../config/gcp-secret-manager.properties | 34 --
.../ofbiz-component.xml | 30 --
.../GcpSecretManagerSecretsProvider.java | 201 ---------
.../ofbiz/gcpsecretmanager/GcpSecretReader.java | 35 --
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../GcpSecretManagerSecretsProviderTest.java | 147 -------
hashicorp-vault-secrets-provider/build.gradle | 28 --
.../config/hashicorp-vault-secrets.properties | 66 ---
.../ofbiz-component.xml | 30 --
.../ofbiz/hashicorpvault/HashicorpVaultReader.java | 39 --
.../HashicorpVaultSecretsProvider.java | 226 ----------
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../HashicorpVaultSecretsProviderTest.java | 156 -------
onepassword-secrets-provider/build.gradle | 23 -
.../config/onepassword.properties | 56 ---
onepassword-secrets-provider/ofbiz-component.xml | 30 --
.../ofbiz/onepassword/OnePasswordHttpClient.java | 39 --
.../onepassword/OnePasswordSecretsProvider.java | 275 ------------
.../org.apache.ofbiz.base.secret.SecretProvider | 1 -
.../OnePasswordSecretsProviderTest.java | 156 -------
41 files changed, 3378 deletions(-)
diff --git a/aws-secrets-provider/build.gradle b/aws-secrets-provider/build.gradle
deleted file mode 100644
index 48c5c3dd7..000000000
--- a/aws-secrets-provider/build.gradle
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// AWS SDK for Java v2 — Apache License 2.0
-// https://github.com/aws/aws-sdk-java-v2
-dependencies {
- pluginLibsCompile 'software.amazon.awssdk:secretsmanager:2.26.31'
- pluginLibsCompile 'software.amazon.awssdk:url-connection-client:2.26.31'
-}
-
-// AWS SDK v2 bundles several items that conflict with OFBiz's global exclusions
-configurations.all {
- exclude group: 'commons-logging', module: 'commons-logging'
-}
diff --git a/aws-secrets-provider/config/aws-secrets-manager.properties b/aws-secrets-provider/config/aws-secrets-manager.properties
deleted file mode 100644
index ba521ee71..000000000
--- a/aws-secrets-provider/config/aws-secrets-manager.properties
+++ /dev/null
@@ -1,53 +0,0 @@
-###############################################################################
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-###############################################################################
-
-####
-# AWS Secrets Manager — SecretProvider configuration
-#
-# Authentication uses the AWS Default Credential Provider Chain:
-# 1. Environment variables: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
-# 2. Java system properties: aws.accessKeyId / aws.secretAccessKey
-# 3. AWS credential profiles file (~/.aws/credentials)
-# 4. EC2 / ECS instance profile / IAM role (recommended for production)
-# 5. AWS SSO
-#
-# No credentials should ever be stored in this file.
-####
-
-# AWS region for the Secrets Manager endpoint.
-# Leave empty to let the SDK resolve from the environment (recommended for EC2/ECS).
-aws.secretsmanager.region=us-east-1
-
-# How long (in seconds) to cache a resolved secret value in memory before re-fetching.
-# Default: 3600 (1 hour). Set to 0 to disable caching (not recommended in production).
-aws.secretsmanager.cache.ttl.seconds=3600
-
-# Optional prefix prepended to every OFBiz secret key before the AWS lookup.
-# Example: with prefix "myapp/prod/" the key "jdbc-password.ofbiz" becomes
-# "myapp/prod/jdbc-password.ofbiz" in AWS Secrets Manager.
-aws.secretsmanager.secret.name.prefix=
-
-# If the secret in AWS is a JSON object (e.g. {"password":"s3cr3t","username":"dbuser"}),
-# set this to the field name that holds the actual secret value (e.g. "password").
-# Leave empty to use the raw secret string as-is.
-aws.secretsmanager.json.field=
-
-# Optional endpoint override. Useful for local testing with LocalStack.
-# Example: http://localhost:4566
-aws.secretsmanager.endpoint.override=
diff --git a/aws-secrets-provider/ofbiz-component.xml b/aws-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index 445cfb3ae..000000000
--- a/aws-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="aws-secrets-provider" enabled="true"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so aws-secrets-manager.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/aws-secrets-provider/src/main/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProvider.java b/aws-secrets-provider/src/main/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProvider.java
deleted file mode 100644
index 83c2fbb83..000000000
--- a/aws-secrets-provider/src/main/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProvider.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.awssecrets;
-
-import java.net.URI;
-import java.util.concurrent.ConcurrentHashMap;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
-import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
-import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
-import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
-import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
-import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerException;
-
-/**
- * {@link SecretProvider} implementation backed by AWS Secrets Manager.
- *
- * <p>Authentication is handled by the AWS Default Credential Provider Chain —
- * no credentials are stored in this file or in properties files. The preferred
- * approach for EC2/ECS/EKS deployments is an IAM instance role or task role,
- * which requires zero credential configuration on the server.</p>
- *
- * <p>Resolved secret values are cached in memory for the duration configured by
- * {@code aws.secretsmanager.cache.ttl.seconds} (default 1 hour) to avoid
- * per-request API calls. Call {@link #invalidateCache()} to force an immediate
- * re-fetch, for example after a manual secret rotation.</p>
- *
- * <p>Configure via {@code plugins/aws-secrets-provider/config/aws-secrets-manager.properties}.</p>
- */
-@ThreadSafe
-public final class AwsSecretsManagerProvider implements SecretProvider {
-
- private static final String MODULE = AwsSecretsManagerProvider.class.getName();
- private static final String CONFIG_RESOURCE = "aws-secrets-manager";
-
- private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
-
- private final SecretsManagerClient client;
- private final long cacheTtlMs;
- private final String secretNamePrefix;
- private final String jsonField;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- private final String value;
- private final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- String getValue() {
- return value;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public AwsSecretsManagerProvider() {
- this(buildClient(), readTtlMs(),
- UtilProperties.getPropertyValue(CONFIG_RESOURCE, "aws.secretsmanager.secret.name.prefix", ""),
- UtilProperties.getPropertyValue(CONFIG_RESOURCE, "aws.secretsmanager.json.field", ""));
- }
-
- /** Package-private constructor used by unit tests to inject a mock client. */
- AwsSecretsManagerProvider(SecretsManagerClient client, long cacheTtlMs,
- String secretNamePrefix, String jsonField) {
- this.client = client;
- this.cacheTtlMs = cacheTtlMs;
- this.secretNamePrefix = secretNamePrefix;
- this.jsonField = jsonField;
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.getValue();
- }
-
- String secretName = secretNamePrefix + key;
- String secretValue = fetchFromAws(secretName);
-
- if (!jsonField.isEmpty()) {
- secretValue = extractJsonField(secretValue, secretName);
- }
-
- if (secretValue == null || secretValue.isEmpty()) {
- throw new GeneralException("Secret '" + secretName + "' resolved to an empty value");
- }
-
- cache.put(key, new CacheEntry(secretValue, cacheTtlMs));
- return secretValue;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * for each key to re-fetch from AWS Secrets Manager. Useful after a manual
- * secret rotation to pick up the new value without restarting OFBiz.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("AwsSecretsManagerProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private String fetchFromAws(String secretName) throws GeneralException {
- try {
- GetSecretValueResponse response = client.getSecretValue(
- GetSecretValueRequest.builder().secretId(secretName).build());
- String value = response.secretString();
- if (value == null) {
- throw new GeneralException("Secret '" + secretName
- + "' contains binary data; only string secrets are supported");
- }
- return value;
- } catch (ResourceNotFoundException e) {
- throw new GeneralException("Secret '" + secretName + "' not found in AWS Secrets Manager", e);
- } catch (SecretsManagerException e) {
- throw new GeneralException("AWS Secrets Manager error for '" + secretName + "': " + e.getMessage(), e);
- }
- }
-
- /**
- * Extracts a single string field from a flat JSON secret value.
- * AWS typically stores database credentials as {"username":"u","password":"p"}.
- */
- private String extractJsonField(String json, String secretName) throws GeneralException {
- try {
- JsonNode root = JSON_MAPPER.readTree(json);
- JsonNode node = root.get(jsonField);
- if (node == null || node.isNull()) {
- throw new GeneralException(
- "JSON field '" + jsonField + "' not found in secret '" + secretName + "'");
- }
- return node.asText();
- } catch (JsonProcessingException e) {
- throw new GeneralException("Failed to parse JSON for secret '" + secretName + "': " + e.getMessage(), e);
- }
- }
-
- private static SecretsManagerClient buildClient() {
- String region = UtilProperties.getPropertyValue(CONFIG_RESOURCE, "aws.secretsmanager.region", "");
- String endpointOverride = UtilProperties.getPropertyValue(CONFIG_RESOURCE,
- "aws.secretsmanager.endpoint.override", "");
-
- SecretsManagerClientBuilder builder = SecretsManagerClient.builder()
- .httpClient(UrlConnectionHttpClient.builder().build());
-
- if (!region.isEmpty()) {
- builder.region(Region.of(region));
- }
- if (!endpointOverride.isEmpty()) {
- builder.endpointOverride(URI.create(endpointOverride));
- }
-
- Debug.logInfo("AwsSecretsManagerProvider: initialized"
- + (region.isEmpty() ? " (region from environment)" : " region=" + region), MODULE);
- return builder.build();
- }
-
- private static long readTtlMs() {
- String raw = UtilProperties.getPropertyValue(CONFIG_RESOURCE,
- "aws.secretsmanager.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid aws.secretsmanager.cache.ttl.seconds '" + raw
- + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-}
diff --git a/aws-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/aws-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 8d42eba0c..000000000
--- a/aws-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.awssecrets.AwsSecretsManagerProvider
diff --git a/aws-secrets-provider/src/test/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProviderTest.java b/aws-secrets-provider/src/test/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProviderTest.java
deleted file mode 100644
index fb6f64b69..000000000
--- a/aws-secrets-provider/src/test/java/org/apache/ofbiz/awssecrets/AwsSecretsManagerProviderTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.awssecrets;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
-import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
-import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
-import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
-
-public class AwsSecretsManagerProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
-
- // -- Happy path --
-
- @Test
- public void getSecretReturnsSecretString() throws GeneralException {
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", "s3cr3t");
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "");
-
- assertEquals("s3cr3t", provider.getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecretCachesPreventsSecondAwsCall() throws GeneralException {
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", "s3cr3t");
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "");
-
- provider.getSecret("jdbc-password.mydb");
- provider.getSecret("jdbc-password.mydb"); // second call — should hit cache
-
- verify(client, times(1)).getSecretValue(any(GetSecretValueRequest.class));
- }
-
- @Test
- public void getSecretAppliesSecretNamePrefix() throws GeneralException {
- SecretsManagerClient client = mock(SecretsManagerClient.class);
- ArgumentCaptor<GetSecretValueRequest> captor = ArgumentCaptor.forClass(GetSecretValueRequest.class);
- when(client.getSecretValue(captor.capture()))
- .thenReturn(response("val"));
-
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "myapp/prod/", "");
- provider.getSecret("jdbc-password.ofbiz");
-
- assertEquals("myapp/prod/jdbc-password.ofbiz", captor.getValue().secretId());
- }
-
- @Test
- public void getSecretExtractsJsonFieldWhenConfigured() throws GeneralException {
- String json = "{\"username\":\"dbuser\",\"password\":\"dbpass\"}";
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", json);
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "password");
-
- assertEquals("dbpass", provider.getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecretUsesRawStringWhenNoJsonFieldConfigured() throws GeneralException {
- String rawPassword = "plain-text-password";
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", rawPassword);
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "");
-
- assertEquals("plain-text-password", provider.getSecret("jdbc-password.mydb"));
- }
-
- // -- Cache invalidation --
-
- @Test
- public void invalidateCacheForcesRefetchOnNextCall() throws GeneralException {
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", "val");
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "");
-
- provider.getSecret("jdbc-password.mydb");
- provider.invalidateCache();
- provider.getSecret("jdbc-password.mydb");
-
- verify(client, times(2)).getSecretValue(any(GetSecretValueRequest.class));
- }
-
- @Test
- public void getSecretExpiredCacheEntryTriggersRefetch() throws GeneralException {
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", "val");
- // TTL of -1 ms means the entry expires immediately (expiresAt is in the past)
- AwsSecretsManagerProvider provider = new AwsSecretsManagerProvider(client, -1L, "", "");
-
- provider.getSecret("jdbc-password.mydb");
- provider.getSecret("jdbc-password.mydb"); // cache entry is expired — must re-fetch
-
- verify(client, times(2)).getSecretValue(any(GetSecretValueRequest.class));
- }
-
- // -- Error handling --
-
- @Test(expected = GeneralException.class)
- public void getSecretThrowsWhenSecretNotFound() throws GeneralException {
- SecretsManagerClient client = mock(SecretsManagerClient.class);
- when(client.getSecretValue(any(GetSecretValueRequest.class)))
- .thenThrow(ResourceNotFoundException.builder().message("not found").build());
-
- new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "")
- .getSecret("jdbc-password.missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecretThrowsWhenJsonFieldMissing() throws GeneralException {
- String json = "{\"username\":\"dbuser\"}"; // no "password" field
- SecretsManagerClient client = clientReturning("jdbc-password.mydb", json);
-
- new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "password")
- .getSecret("jdbc-password.mydb");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecretThrowsWhenSecretStringIsNull() throws GeneralException {
- SecretsManagerClient client = mock(SecretsManagerClient.class);
- // secretString() returns null — this is a binary secret
- when(client.getSecretValue(any(GetSecretValueRequest.class)))
- .thenReturn(GetSecretValueResponse.builder().build());
-
- new AwsSecretsManagerProvider(client, ONE_HOUR_MS, "", "")
- .getSecret("binary-secret");
- }
-
- // -- helpers --
-
- private static SecretsManagerClient clientReturning(String secretId, String secretValue) {
- SecretsManagerClient client = mock(SecretsManagerClient.class);
- when(client.getSecretValue(any(GetSecretValueRequest.class)))
- .thenReturn(response(secretValue));
- return client;
- }
-
- private static GetSecretValueResponse response(String secretValue) {
- return GetSecretValueResponse.builder().secretString(secretValue).build();
- }
-}
diff --git a/azure-keyvault-secrets-provider/build.gradle b/azure-keyvault-secrets-provider/build.gradle
deleted file mode 100644
index 9f36ae2e2..000000000
--- a/azure-keyvault-secrets-provider/build.gradle
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// Azure SDK for Java — Key Vault Secrets (MIT License)
-// https://github.com/Azure/azure-sdk-for-java
-dependencies {
- pluginLibsCompile 'com.azure:azure-security-keyvault-secrets:4.8.0'
- pluginLibsCompile 'com.azure:azure-identity:1.12.0'
-}
-
-configurations.all {
- exclude group: 'commons-logging', module: 'commons-logging'
-}
diff --git a/azure-keyvault-secrets-provider/config/azure-keyvault.properties b/azure-keyvault-secrets-provider/config/azure-keyvault.properties
deleted file mode 100644
index fd8f262a6..000000000
--- a/azure-keyvault-secrets-provider/config/azure-keyvault.properties
+++ /dev/null
@@ -1,39 +0,0 @@
-###############################################################################
-# Azure Key Vault — SecretProvider configuration
-#
-# To activate this provider:
-# 1. Set enabled="true" in azure-keyvault-secrets-provider/ofbiz-component.xml
-# 2. Set enabled="false" on every other *-secrets-provider plugin
-###############################################################################
-
-# Full URL of your Azure Key Vault (required)
-# Example: https://my-vault.vault.azure.net
-azure.keyvault.url=
-
-# Authentication method:
-# default — Uses DefaultAzureCredential: tries Managed Identity, env vars,
-# Azure CLI, Visual Studio Code, etc. in order. Recommended for
-# Azure-hosted deployments (AKS, App Service, VMs with Managed Identity).
-# client_secret — Authenticates as a service principal using tenant + client credentials.
-# Use for on-premise or non-Azure deployments.
-azure.auth.method=default
-
-# Required only when azure.auth.method=client_secret
-azure.tenant.id=
-azure.client.id=
-azure.client.secret=
-
-# Optional prefix prepended to every secret name before lookup.
-# Example: "prod-" makes key "db" → "prod-db"
-azure.secret.name.prefix=
-
-# Azure Key Vault secret names may only contain letters, digits and hyphens.
-# OFBiz keys like "jdbc-password.mysql-ofbiz" contain a dot, which is invalid.
-# Set this to the replacement character (default: -) so the dot is substituted.
-# Example: "jdbc-password.mysql-ofbiz" → "jdbc-password-mysql-ofbiz"
-# Set to empty to disable replacement (only do this if your keys have no dots).
-azure.secret.name.dot.replacement=-
-
-# In-memory cache TTL in seconds (default: 3600 = 1 hour).
-# Set to 0 to disable caching (fetches from Azure on every call).
-azure.cache.ttl.seconds=3600
diff --git a/azure-keyvault-secrets-provider/ofbiz-component.xml b/azure-keyvault-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index 65ed6fdfd..000000000
--- a/azure-keyvault-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="azure-keyvault-secrets-provider" enabled="false"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so azure-keyvault.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultReader.java b/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultReader.java
deleted file mode 100644
index a268df676..000000000
--- a/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultReader.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.azurekeyvault;
-
-/**
- * Thin seam over Azure Key Vault reads used by {@link AzureKeyVaultSecretsProvider}.
- * Kept package-private so tests can substitute a lambda without connecting to Azure.
- */
-@FunctionalInterface
-interface AzureKeyVaultReader {
- /**
- * Retrieves the current value of the named secret.
- *
- * @param secretName the Key Vault secret name (letters, digits and hyphens only)
- * @return the plaintext secret value; never {@code null}
- * @throws Exception if the read fails or the secret does not exist
- */
- String read(String secretName) throws Exception;
-}
diff --git a/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProvider.java b/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProvider.java
deleted file mode 100644
index b32a8b2d3..000000000
--- a/azure-keyvault-secrets-provider/src/main/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProvider.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.azurekeyvault;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-import com.azure.core.credential.TokenCredential;
-import com.azure.identity.ClientSecretCredentialBuilder;
-import com.azure.identity.DefaultAzureCredentialBuilder;
-import com.azure.security.keyvault.secrets.SecretClient;
-import com.azure.security.keyvault.secrets.SecretClientBuilder;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-/**
- * {@link SecretProvider} implementation backed by Azure Key Vault.
- *
- * <h2>Authentication</h2>
- * <p>Controlled by {@code azure.auth.method}:</p>
- * <ul>
- * <li><strong>default</strong> — Uses {@code DefaultAzureCredential}, which tries
- * Managed Identity, environment variables, Azure CLI, and more in sequence.
- * Recommended for Azure-hosted deployments (AKS, App Service, VMs with MI).</li>
- * <li><strong>client_secret</strong> — Authenticates as a service principal using
- * {@code azure.tenant.id}, {@code azure.client.id}, and {@code azure.client.secret}.
- * Suitable for on-premise or multi-cloud deployments.</li>
- * </ul>
- *
- * <h2>Secret name mapping</h2>
- * <p>Azure Key Vault secret names may only contain letters, digits and hyphens.
- * OFBiz keys contain dots (e.g. {@code jdbc-password.mysql-ofbiz}), which are invalid.
- * Set {@code azure.secret.name.dot.replacement=-} (the default) to replace dots with
- * hyphens, so the key maps to {@code jdbc-password-mysql-ofbiz} in the vault.</p>
- *
- * <p>Configure via {@code plugins/azure-keyvault-secrets-provider/config/azure-keyvault.properties}.</p>
- */
-@ThreadSafe
-public final class AzureKeyVaultSecretsProvider implements SecretProvider {
-
- private static final String MODULE = AzureKeyVaultSecretsProvider.class.getName();
- private static final String CONFIG_RESOURCE = "azure-keyvault";
-
- private final AzureKeyVaultReader vaultReader;
- private final String secretNamePrefix;
- private final String dotReplacement;
- private final long cacheTtlMs;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- final String value;
- final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public AzureKeyVaultSecretsProvider() {
- this(readerFrom(buildClient()),
- prop("azure.secret.name.prefix", ""),
- prop("azure.secret.name.dot.replacement", "-"),
- readTtlMs());
- }
-
- /** Package-private constructor used by unit tests to inject an {@link AzureKeyVaultReader} lambda. */
- AzureKeyVaultSecretsProvider(AzureKeyVaultReader vaultReader, String secretNamePrefix,
- String dotReplacement, long cacheTtlMs) {
- this.vaultReader = vaultReader;
- this.secretNamePrefix = secretNamePrefix;
- this.dotReplacement = dotReplacement;
- this.cacheTtlMs = cacheTtlMs;
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.value;
- }
-
- String sanitizedKey = dotReplacement.isEmpty() ? key : key.replace(".", dotReplacement);
- String secretName = secretNamePrefix + sanitizedKey;
-
- String value;
- try {
- value = vaultReader.read(secretName);
- } catch (Exception e) {
- throw new GeneralException(
- "Azure Key Vault read failed for secret '" + secretName + "': " + e.getMessage(), e);
- }
-
- if (value == null || value.isEmpty()) {
- throw new GeneralException(
- "Azure Key Vault returned empty value for secret '" + secretName + "'");
- }
-
- cache.put(key, new CacheEntry(value, cacheTtlMs));
- return value;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * to re-fetch from Azure Key Vault. Useful after a secret rotation.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("AzureKeyVaultSecretsProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private static AzureKeyVaultReader readerFrom(SecretClient client) {
- return secretName -> client.getSecret(secretName).getValue();
- }
-
- private static SecretClient buildClient() {
- String vaultUrl = prop("azure.keyvault.url", "");
- String authMethod = prop("azure.auth.method", "default");
-
- TokenCredential credential;
- if ("client_secret".equalsIgnoreCase(authMethod)) {
- credential = new ClientSecretCredentialBuilder()
- .tenantId(prop("azure.tenant.id", ""))
- .clientId(prop("azure.client.id", ""))
- .clientSecret(prop("azure.client.secret", ""))
- .build();
- } else {
- credential = new DefaultAzureCredentialBuilder().build();
- }
-
- Debug.logInfo("AzureKeyVaultSecretsProvider: initialized vault=" + vaultUrl
- + " auth=" + authMethod, MODULE);
-
- return new SecretClientBuilder()
- .vaultUrl(vaultUrl)
- .credential(credential)
- .buildClient();
- }
-
- private static long readTtlMs() {
- String raw = prop("azure.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid azure.cache.ttl.seconds '" + raw + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-
- private static String prop(String key, String defaultValue) {
- return UtilProperties.getPropertyValue(CONFIG_RESOURCE, key, defaultValue);
- }
-}
diff --git a/azure-keyvault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/azure-keyvault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 3aac1713e..000000000
--- a/azure-keyvault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.azurekeyvault.AzureKeyVaultSecretsProvider
diff --git a/azure-keyvault-secrets-provider/src/test/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProviderTest.java b/azure-keyvault-secrets-provider/src/test/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProviderTest.java
deleted file mode 100644
index 9f036f2dc..000000000
--- a/azure-keyvault-secrets-provider/src/test/java/org/apache/ofbiz/azurekeyvault/AzureKeyVaultSecretsProviderTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.azurekeyvault;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.Test;
-
-public class AzureKeyVaultSecretsProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
-
- // -- Happy path --
-
- @Test
- public void getSecret_returnsSecretValue() throws GeneralException {
- AzureKeyVaultReader reader = fixedReader("mykey", "s3cr3t");
- assertEquals("s3cr3t", provider(reader, "", "-").getSecret("mykey"));
- }
-
- @Test
- public void getSecret_sanitizesDotInKey() throws GeneralException {
- // OFBiz key "jdbc-password.mysql-ofbiz" → Azure name "jdbc-password-mysql-ofbiz"
- AzureKeyVaultReader reader = fixedReader("jdbc-password-mysql-ofbiz", "dbpass");
- assertEquals("dbpass", provider(reader, "", "-").getSecret("jdbc-password.mysql-ofbiz"));
- }
-
- @Test
- public void getSecret_appliesPrefixAfterSanitizing() throws GeneralException {
- AzureKeyVaultReader reader = fixedReader("prod-jdbc-password-mydb", "prodpass");
- assertEquals("prodpass", provider(reader, "prod-", "-").getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_cachePreventsDuplicateReaderCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- AzureKeyVaultReader reader = secretName -> {
- calls.incrementAndGet();
- return "val";
- };
- AzureKeyVaultSecretsProvider p = provider(reader, "", "-");
-
- p.getSecret("mykey");
- p.getSecret("mykey"); // cache hit
-
- assertEquals(1, calls.get());
- }
-
- @Test
- public void invalidateCache_forcesRefetchOnNextCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- AzureKeyVaultReader reader = secretName -> {
- calls.incrementAndGet();
- return "val";
- };
- AzureKeyVaultSecretsProvider p = provider(reader, "", "-");
-
- p.getSecret("mykey");
- p.invalidateCache();
- p.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- @Test
- public void getSecret_expiredCacheTriggersRefetch() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- AzureKeyVaultReader reader = secretName -> {
- calls.incrementAndGet();
- return "val";
- };
- // TTL of -1 ms — entries expire immediately
- AzureKeyVaultSecretsProvider p = new AzureKeyVaultSecretsProvider(
- reader, "", "-", -1L);
-
- p.getSecret("mykey");
- p.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- @Test
- public void getSecret_dotReplacementDisabled_keepsDotsInName() throws GeneralException {
- AzureKeyVaultReader reader = fixedReader("my.key", "val");
- assertEquals("val", provider(reader, "", "").getSecret("my.key"));
- }
-
- // -- Error handling --
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnReaderException() throws GeneralException {
- AzureKeyVaultReader reader = secretName -> { throw new RuntimeException("SecretNotFound"); };
- provider(reader, "", "-").getSecret("missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnEmptyValue() throws GeneralException {
- AzureKeyVaultReader reader = secretName -> "";
- provider(reader, "", "-").getSecret("mykey");
- }
-
- // -- helpers --
-
- private static AzureKeyVaultSecretsProvider provider(AzureKeyVaultReader reader,
- String prefix, String dotReplacement) {
- return new AzureKeyVaultSecretsProvider(reader, prefix, dotReplacement, ONE_HOUR_MS);
- }
-
- private static AzureKeyVaultReader fixedReader(String expectedName, String value) {
- return secretName -> expectedName.equals(secretName) ? value : null;
- }
-}
diff --git a/bitwarden-secrets-provider/build.gradle b/bitwarden-secrets-provider/build.gradle
deleted file mode 100644
index be905aee6..000000000
--- a/bitwarden-secrets-provider/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// No external SDK required.
-// Uses Java's built-in java.net.http.HttpClient (Java 11+), javax.crypto for
-// AES-256-CBC + HMAC-SHA256 decryption, and OFBiz's bundled jackson-databind.
-//
-// Bitwarden Secrets Manager encrypts all data end-to-end. The symmetric key
-// embedded in the machine account access token is used to decrypt secret names
-// and values client-side.
-dependencies {}
diff --git a/bitwarden-secrets-provider/config/bitwarden-secrets.properties b/bitwarden-secrets-provider/config/bitwarden-secrets.properties
deleted file mode 100644
index 6552fa9c3..000000000
--- a/bitwarden-secrets-provider/config/bitwarden-secrets.properties
+++ /dev/null
@@ -1,59 +0,0 @@
-###############################################################################
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-###############################################################################
-
-####
-# Bitwarden Secrets Manager — SecretProvider configuration
-#
-# Requires a Bitwarden Secrets Manager machine account access token.
-# Create one in the Bitwarden SM console under Machine Accounts.
-#
-# The access token embeds an AES-256 symmetric key used to decrypt
-# secret names and values client-side (end-to-end encryption).
-#
-# NOTE: Only ONE SecretProvider plugin may be active at a time.
-# Deploy only the plugin that matches your environment.
-####
-
-# Bitwarden Secrets Manager REST API base URL.
-# Use https://api.bitwarden.eu for EU cloud, or your self-hosted URL.
-bitwarden.api.url=https://api.bitwarden.com
-
-# Bitwarden Identity Service URL (used for machine account OAuth token exchange).
-bitwarden.identity.url=https://identity.bitwarden.com
-
-# Machine account access token.
-# Format: 0.<serviceAccountId>.<clientSecret>:<base64EncKey>
-# Inject at deploy time — do not commit a real token.
-bitwarden.access.token=
-
-# UUID of the organization that owns the secrets.
-bitwarden.organization.id=
-
-# Optional prefix prepended to the OFBiz key when matching the Bitwarden secret key (name).
-# Example: with prefix "myapp/" the OFBiz key "jdbc-password.ofbiz" matches a
-# Bitwarden secret whose decrypted key is "myapp/jdbc-password.ofbiz".
-bitwarden.secret.name.prefix=
-
-# How long (in seconds) to cache resolved secret values before re-fetching.
-# Default: 3600 (1 hour). Set to 0 to disable caching.
-bitwarden.cache.ttl.seconds=3600
-
-# HTTP connect and read timeouts in seconds.
-bitwarden.connect.timeout.seconds=5
-bitwarden.read.timeout.seconds=10
diff --git a/bitwarden-secrets-provider/ofbiz-component.xml b/bitwarden-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index c5c3a8aec..000000000
--- a/bitwarden-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="bitwarden-secrets-provider" enabled="false"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so bitwarden-secrets.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenHttpClient.java b/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenHttpClient.java
deleted file mode 100644
index 64260a519..000000000
--- a/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenHttpClient.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.bitwarden;
-
-import java.io.IOException;
-
-/**
- * Thin seam over HTTP calls used by {@link BitwardenSecretsProvider}.
- * Kept package-private so tests can substitute a mock without making real
- * network calls.
- */
-interface BitwardenHttpClient {
-
- /**
- * Performs an authenticated GET request.
- *
- * @param url the full request URL
- * @param bearerToken the OAuth bearer token
- * @return the response body as a UTF-8 string
- * @throws IOException if the request fails or the server returns a non-2xx status
- */
- String get(String url, String bearerToken) throws IOException;
-
- /**
- * Performs a form-encoded POST request (no auth header — used for token exchange).
- *
- * @param url the full request URL
- * @param formBody {@code application/x-www-form-urlencoded} encoded body
- * @return the response body as a UTF-8 string
- * @throws IOException if the request fails or the server returns a non-2xx status
- */
- String post(String url, String formBody) throws IOException;
-}
diff --git a/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProvider.java b/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProvider.java
deleted file mode 100644
index ed94ea7a8..000000000
--- a/bitwarden-secrets-provider/src/main/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProvider.java
+++ /dev/null
@@ -1,461 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.bitwarden;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-/**
- * {@link SecretProvider} implementation backed by Bitwarden Secrets Manager.
- *
- * <h2>Authentication</h2>
- * <p>Uses a machine account <em>access token</em> (created in the Bitwarden SM console).
- * The token has the format {@code 0.<serviceAccountId>.<clientSecret>:<base64EncKey>}.
- * The provider parses the token to extract OAuth credentials for the identity service
- * and the 64-byte symmetric key for client-side decryption.</p>
- *
- * <h2>End-to-end encryption</h2>
- * <p>Bitwarden SM encrypts all data at rest and in transit. Secret <em>keys</em> (names)
- * and <em>values</em> are returned from the API in encrypted form. This provider decrypts
- * them using AES-256-CBC with HMAC-SHA256 integrity verification before returning the
- * plaintext to OFBiz. No plaintext ever leaves the JVM.</p>
- *
- * <h2>Cipher format</h2>
- * <p>Encrypted strings use Bitwarden's type-2 cipher format:
- * {@code 2.<iv_base64>|<ciphertext_base64>|<hmac_base64>}</p>
- * <ul>
- * <li>MAC key (bytes 32–63 of the 64-byte symmetric key) verifies integrity via
- * HMAC-SHA256 over {@code IV || ciphertext}.</li>
- * <li>Enc key (bytes 0–31) decrypts with AES-256-CBC after MAC is verified.</li>
- * </ul>
- *
- * <h2>API flow per lookup</h2>
- * <ol>
- * <li>POST to identity service to exchange client credentials for a bearer token.</li>
- * <li>GET the organization's secret list; decrypt each secret key to find the match.</li>
- * <li>GET the matched secret by ID; decrypt and return the value.</li>
- * </ol>
- *
- * <p>Configure via {@code plugins/bitwarden-secrets-provider/config/bitwarden-secrets.properties}.</p>
- */
-@ThreadSafe
-public final class BitwardenSecretsProvider implements SecretProvider {
-
- private static final String MODULE = BitwardenSecretsProvider.class.getName();
- private static final String CONFIG_RESOURCE = "bitwarden-secrets";
- private static final ObjectMapper JSON = new ObjectMapper();
-
- private final BitwardenHttpClient httpClient;
- private final String apiUrl;
- private final String identityUrl;
- private final String organizationId;
- private final String secretNamePrefix;
- private final long cacheTtlMs;
-
- // Parsed from the access token — never stored in config
- private final String oauthClientId;
- private final String oauthClientSecret;
- private final byte[] encKey; // bytes 0-31 of the 64-byte symmetric key
- private final byte[] macKey; // bytes 32-63
-
- // Cached OAuth bearer token + its expiry
- private volatile String bearerToken = null;
- private volatile long bearerTokenExpiresAt = 0L;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- final String value;
- final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public BitwardenSecretsProvider() throws GeneralException {
- this(buildHttpClient(),
- prop("bitwarden.api.url", "https://api.bitwarden.com").replaceAll("/+$", ""),
- prop("bitwarden.identity.url", "https://identity.bitwarden.com").replaceAll("/+$", ""),
- prop("bitwarden.organization.id", ""),
- prop("bitwarden.secret.name.prefix", ""),
- readTtlMs(),
- prop("bitwarden.access.token", ""));
- }
-
- /**
- * Package-private constructor used by unit tests to inject mock HTTP client and
- * pre-parsed key material.
- */
- BitwardenSecretsProvider(BitwardenHttpClient httpClient, String apiUrl, String identityUrl,
- String organizationId, String secretNamePrefix, long cacheTtlMs,
- String clientId, String clientSecret, byte[] encKey, byte[] macKey) {
- this.httpClient = httpClient;
- this.apiUrl = apiUrl;
- this.identityUrl = identityUrl;
- this.organizationId = organizationId;
- this.secretNamePrefix = secretNamePrefix;
- this.cacheTtlMs = cacheTtlMs;
- this.oauthClientId = clientId;
- this.oauthClientSecret = clientSecret;
- this.encKey = encKey;
- this.macKey = macKey;
- }
-
- /** Full constructor called by the public no-arg constructor after parsing the token. */
- private BitwardenSecretsProvider(BitwardenHttpClient httpClient, String apiUrl, String identityUrl,
- String organizationId, String secretNamePrefix, long cacheTtlMs,
- String rawAccessToken) throws GeneralException {
- this.httpClient = httpClient;
- this.apiUrl = apiUrl;
- this.identityUrl = identityUrl;
- this.organizationId = organizationId;
- this.secretNamePrefix = secretNamePrefix;
- this.cacheTtlMs = cacheTtlMs;
-
- // Parse: "0.<serviceAccountId>.<clientSecret>:<base64EncKey>"
- int colonIdx = rawAccessToken.lastIndexOf(':');
- if (colonIdx < 0) {
- throw new GeneralException(
- "Invalid Bitwarden access token format — missing ':' separator");
- }
- String identityPart = rawAccessToken.substring(0, colonIdx);
- String encKeyBase64 = rawAccessToken.substring(colonIdx + 1);
-
- String[] dotParts = identityPart.split("\\.");
- if (dotParts.length != 3 || !"0".equals(dotParts[0])) {
- throw new GeneralException(
- "Invalid Bitwarden access token format — expected '0.<id>.<secret>:<key>'");
- }
- String serviceAccountId = dotParts[1];
- String clientSecretPart = dotParts[2];
-
- this.oauthClientId = "service-account." + serviceAccountId;
- this.oauthClientSecret = clientSecretPart;
-
- byte[] keyBytes;
- try {
- keyBytes = Base64.getDecoder().decode(encKeyBase64);
- } catch (IllegalArgumentException e) {
- throw new GeneralException("Invalid Bitwarden access token — enc key is not valid base64", e);
- }
- if (keyBytes.length != 64) {
- throw new GeneralException("Invalid Bitwarden access token — enc key must be 64 bytes, got "
- + keyBytes.length);
- }
- this.encKey = Arrays.copyOfRange(keyBytes, 0, 32);
- this.macKey = Arrays.copyOfRange(keyBytes, 32, 64);
-
- Debug.logInfo("BitwardenSecretsProvider: initialized api=" + apiUrl
- + " org=" + organizationId, MODULE);
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.value;
- }
-
- String secretName = secretNamePrefix + key;
- String value = fetchFromBitwarden(secretName);
-
- cache.put(key, new CacheEntry(value, cacheTtlMs));
- return value;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * to re-fetch and re-decrypt from Bitwarden SM.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("BitwardenSecretsProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private String fetchFromBitwarden(String secretName) throws GeneralException {
- String bearer = ensureBearerToken();
- String secretId = findSecretId(secretName, bearer);
- return fetchAndDecryptValue(secretId, secretName, bearer);
- }
-
- /** Returns a valid OAuth bearer token, refreshing it if expired. */
- private synchronized String ensureBearerToken() throws GeneralException {
- if (bearerToken != null && System.currentTimeMillis() < bearerTokenExpiresAt) {
- return bearerToken;
- }
-
- String body = "grant_type=client_credentials"
- + "&scope=api.secrets"
- + "&client_id=" + oauthClientId
- + "&client_secret=" + oauthClientSecret;
-
- String responseJson;
- try {
- responseJson = httpClient.post(identityUrl + "/connect/token", body);
- } catch (IOException e) {
- throw new GeneralException("Bitwarden identity token request failed: " + e.getMessage(), e);
- }
-
- try {
- JsonNode root = JSON.readTree(responseJson);
- JsonNode tokenNode = root.get("access_token");
- JsonNode expiresNode = root.get("expires_in");
- if (tokenNode == null || tokenNode.isNull()) {
- throw new GeneralException("Bitwarden identity response missing 'access_token'");
- }
- bearerToken = tokenNode.asText();
- long expiresIn = expiresNode != null ? expiresNode.asLong(3600L) : 3600L;
- // Subtract 60 s to renew slightly before expiry
- bearerTokenExpiresAt = System.currentTimeMillis() + (expiresIn - 60L) * 1000L;
- return bearerToken;
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Failed to parse Bitwarden identity response: " + e.getMessage(), e);
- }
- }
-
- /** Lists the organization's secrets and returns the ID of the one matching secretName. */
- private String findSecretId(String secretName, String bearer) throws GeneralException {
- String listUrl = apiUrl + "/organizations/" + organizationId + "/secrets";
- String responseJson;
- try {
- responseJson = httpClient.get(listUrl, bearer);
- } catch (IOException e) {
- throw new GeneralException("Bitwarden secrets list request failed: " + e.getMessage(), e);
- }
-
- try {
- JsonNode root = JSON.readTree(responseJson);
- JsonNode data = root.get("data");
- if (data == null || !data.isArray()) {
- throw new GeneralException("Bitwarden secrets list response missing 'data' array");
- }
-
- for (JsonNode item : data) {
- JsonNode encKey = item.get("key");
- if (encKey == null) continue;
-
- String decryptedKey = decrypt(encKey.asText());
- if (secretName.equals(decryptedKey)) {
- JsonNode idNode = item.get("id");
- if (idNode == null || idNode.isNull()) {
- throw new GeneralException("Bitwarden secret '" + secretName + "' has no id field");
- }
- return idNode.asText();
- }
- }
- throw new GeneralException("No Bitwarden secret found with key '" + secretName + "'");
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Failed to parse Bitwarden secrets list: " + e.getMessage(), e);
- }
- }
-
- /** Fetches the secret by ID and decrypts its value. */
- private String fetchAndDecryptValue(String secretId, String secretName, String bearer)
- throws GeneralException {
- String secretUrl = apiUrl + "/secrets/" + secretId;
- String responseJson;
- try {
- responseJson = httpClient.get(secretUrl, bearer);
- } catch (IOException e) {
- throw new GeneralException("Bitwarden secret fetch failed for '" + secretName
- + "': " + e.getMessage(), e);
- }
-
- try {
- JsonNode root = JSON.readTree(responseJson);
- JsonNode encValueNode = root.get("value");
- if (encValueNode == null || encValueNode.isNull()) {
- throw new GeneralException("Bitwarden secret '" + secretName + "' has no value field");
- }
- String plaintext = decrypt(encValueNode.asText());
- if (plaintext.isEmpty()) {
- throw new GeneralException("Bitwarden secret '" + secretName + "' decrypted to empty string");
- }
- return plaintext;
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Failed to parse Bitwarden secret response: " + e.getMessage(), e);
- }
- }
-
- /**
- * Decrypts a Bitwarden type-2 cipher string.
- *
- * <p>Format: {@code 2.<iv_b64>|<ciphertext_b64>|<hmac_b64>}</p>
- * <ul>
- * <li>HMAC-SHA256 is verified over {@code IV || ciphertext} using {@code macKey}.</li>
- * <li>AES-256-CBC decryption uses {@code encKey} and the extracted IV.</li>
- * </ul>
- */
- String decrypt(String cipherString) throws GeneralException {
- if (!cipherString.startsWith("2.")) {
- throw new GeneralException(
- "Unsupported Bitwarden cipher type — expected type 2 (AES-CBC-256-HMAC-SHA256)");
- }
- String[] parts = cipherString.substring(2).split("\\|");
- if (parts.length != 3) {
- throw new GeneralException(
- "Invalid Bitwarden cipher string — expected '2.<iv>|<ct>|<hmac>'");
- }
-
- byte[] iv, ciphertext, expectedHmac;
- try {
- iv = Base64.getDecoder().decode(parts[0]);
- ciphertext = Base64.getDecoder().decode(parts[1]);
- expectedHmac = Base64.getDecoder().decode(parts[2]);
- } catch (IllegalArgumentException e) {
- throw new GeneralException("Invalid base64 in Bitwarden cipher string", e);
- }
-
- try {
- // Verify integrity: HMAC-SHA256(iv || ciphertext, macKey)
- Mac mac = Mac.getInstance("HmacSHA256");
- mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
- mac.update(iv);
- byte[] computedHmac = mac.doFinal(ciphertext);
- if (!MessageDigest.isEqual(computedHmac, expectedHmac)) {
- throw new GeneralException(
- "Bitwarden HMAC verification failed — wrong key or tampered ciphertext");
- }
-
- // Decrypt: AES-256-CBC
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.DECRYPT_MODE,
- new SecretKeySpec(encKey, "AES"),
- new IvParameterSpec(iv));
- return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
-
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Bitwarden decryption failed: " + e.getMessage(), e);
- }
- }
-
- private static BitwardenHttpClient buildHttpClient() {
- int connectTimeout = parseSeconds(prop("bitwarden.connect.timeout.seconds", "5"));
- int readTimeout = parseSeconds(prop("bitwarden.read.timeout.seconds", "10"));
-
- HttpClient javaClient = HttpClient.newBuilder()
- .connectTimeout(Duration.ofSeconds(connectTimeout))
- .build();
-
- return new BitwardenHttpClient() {
- @Override
- public String get(String url, String bearerToken) throws IOException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .header("Authorization", "Bearer " + bearerToken)
- .header("Accept", "application/json")
- .timeout(Duration.ofSeconds(readTimeout))
- .GET()
- .build();
- return send(request);
- }
-
- @Override
- public String post(String url, String formBody) throws IOException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .header("Content-Type", "application/x-www-form-urlencoded")
- .header("Accept", "application/json")
- .timeout(Duration.ofSeconds(readTimeout))
- .POST(HttpRequest.BodyPublishers.ofString(formBody, StandardCharsets.UTF_8))
- .build();
- return send(request);
- }
-
- private String send(HttpRequest request) throws IOException {
- HttpResponse<String> response;
- try {
- response = javaClient.send(request,
- HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("HTTP request interrupted", e);
- }
- int status = response.statusCode();
- if (status < 200 || status >= 300) {
- throw new IOException("Bitwarden API returned HTTP " + status
- + " for " + request.uri());
- }
- return response.body();
- }
- };
- }
-
- private static int parseSeconds(String raw) {
- try {
- return Integer.parseInt(raw.trim());
- } catch (NumberFormatException e) {
- return 10;
- }
- }
-
- private static long readTtlMs() {
- String raw = prop("bitwarden.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid bitwarden.cache.ttl.seconds '" + raw + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-
- private static String prop(String key, String defaultValue) {
- return UtilProperties.getPropertyValue(CONFIG_RESOURCE, key, defaultValue);
- }
-}
diff --git a/bitwarden-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/bitwarden-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 8f6a30013..000000000
--- a/bitwarden-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.bitwarden.BitwardenSecretsProvider
diff --git a/bitwarden-secrets-provider/src/test/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProviderTest.java b/bitwarden-secrets-provider/src/test/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProviderTest.java
deleted file mode 100644
index 70e6e0f7b..000000000
--- a/bitwarden-secrets-provider/src/test/java/org/apache/ofbiz/bitwarden/BitwardenSecretsProviderTest.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.bitwarden;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.contains;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Base64;
-
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- * Tests for {@link BitwardenSecretsProvider}.
- *
- * <p>A stable 64-byte test key is generated once for the test class.
- * Helper methods produce properly-encrypted cipher strings so the decryption
- * tests exercise the real AES-256-CBC + HMAC-SHA256 path, not just mocked output.</p>
- */
-public class BitwardenSecretsProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
- private static final String API_URL = "https://api.bitwarden.com";
- private static final String IDENTITY_URL = "https://identity.bitwarden.com";
- private static final String ORG_ID = "org-abc";
- private static final String BEARER_TOKEN_RESPONSE =
- "{\"access_token\":\"test-bearer\",\"expires_in\":3600}";
-
- private static byte[] TEST_ENC_KEY;
- private static byte[] TEST_MAC_KEY;
-
- @BeforeClass
- public static void generateTestKey() {
- byte[] keyBytes = new byte[64];
- new SecureRandom().nextBytes(keyBytes);
- TEST_ENC_KEY = Arrays.copyOfRange(keyBytes, 0, 32);
- TEST_MAC_KEY = Arrays.copyOfRange(keyBytes, 32, 64);
- }
-
- // -- Decryption unit tests (no HTTP, pure crypto) --
-
- @Test
- public void decrypt_roundTrip() throws Exception {
- String plaintext = "my-secret-password";
- String cipherString = encrypt(plaintext);
-
- BitwardenSecretsProvider provider = provider(mockHttpForSecret("mykey", plaintext));
- assertEquals(plaintext, provider.decrypt(cipherString));
- }
-
- @Test(expected = GeneralException.class)
- public void decrypt_throwsOnWrongCipherType() throws Exception {
- BitwardenSecretsProvider provider = provider(mock(BitwardenHttpClient.class));
- provider.decrypt("1.abc|def|ghi"); // type 1, not 2
- }
-
- @Test(expected = GeneralException.class)
- public void decrypt_throwsOnTamperedCiphertext() throws Exception {
- String cipherString = encrypt("secret");
- // Flip a byte in the ciphertext part
- String[] parts = cipherString.substring(2).split("\\|");
- byte[] ct = Base64.getDecoder().decode(parts[1]);
- ct[0] ^= 0xFF;
- String tampered = "2." + parts[0] + "|" + Base64.getEncoder().encodeToString(ct) + "|" + parts[2];
-
- BitwardenSecretsProvider provider = provider(mock(BitwardenHttpClient.class));
- provider.decrypt(tampered);
- }
-
- // -- Full flow tests (with mock HTTP) --
-
- @Test
- public void getSecret_fetchesAndDecryptsSecret() throws Exception {
- String secretValue = "db-password-123";
- BitwardenHttpClient client = mockHttpForSecret("jdbc-password.mydb", secretValue);
- BitwardenSecretsProvider provider = provider(client);
-
- assertEquals(secretValue, provider.getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_appliesSecretNamePrefix() throws Exception {
- String secretValue = "dbpass";
- BitwardenHttpClient client = mockHttpForSecret("prod/jdbc-password.mydb", secretValue);
- BitwardenSecretsProvider provider = providerWithPrefix(client, "prod/");
-
- assertEquals(secretValue, provider.getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_cachePreventsSecondApiCall() throws Exception {
- BitwardenHttpClient client = mockHttpForSecret("mykey", "val");
- BitwardenSecretsProvider provider = provider(client);
-
- provider.getSecret("mykey");
- provider.getSecret("mykey");
-
- // Only 1 identity POST + 1 list GET + 1 secret GET = 3 calls for the first fetch
- verify(client, times(1)).post(anyString(), anyString());
- verify(client, times(1)).get(contains("/organizations/"), anyString());
- verify(client, times(1)).get(contains("/secrets/"), anyString());
- }
-
- @Test
- public void invalidateCache_forcesRefetch() throws Exception {
- BitwardenHttpClient client = mockHttpForSecret("mykey", "val");
- BitwardenSecretsProvider provider = provider(client);
-
- provider.getSecret("mykey");
- provider.invalidateCache();
- provider.getSecret("mykey");
-
- // Two full fetches — bearer token is still valid so POST called once
- verify(client, times(2)).get(contains("/organizations/"), anyString());
- verify(client, times(2)).get(contains("/secrets/"), anyString());
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenSecretNotFound() throws Exception {
- BitwardenHttpClient client = mock(BitwardenHttpClient.class);
- when(client.post(anyString(), anyString())).thenReturn(BEARER_TOKEN_RESPONSE);
- when(client.get(contains("/organizations/"), anyString()))
- .thenReturn("{\"data\":[]}"); // empty list
-
- provider(client).getSecret("missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnHttpError() throws Exception {
- BitwardenHttpClient client = mock(BitwardenHttpClient.class);
- when(client.post(anyString(), anyString())).thenThrow(new IOException("connection refused"));
-
- provider(client).getSecret("mykey");
- }
-
- // -- helpers --
-
- private BitwardenSecretsProvider provider(BitwardenHttpClient client) throws GeneralException {
- return providerWithPrefix(client, "");
- }
-
- private BitwardenSecretsProvider providerWithPrefix(BitwardenHttpClient client, String prefix)
- throws GeneralException {
- return new BitwardenSecretsProvider(client, API_URL, IDENTITY_URL,
- ORG_ID, prefix, ONE_HOUR_MS,
- "service-account.test-id", "test-secret", TEST_ENC_KEY, TEST_MAC_KEY);
- }
-
- /**
- * Builds a mock HTTP client that returns a bearer token on POST and serves a
- * secrets list + secret detail using properly encrypted cipher strings.
- */
- private BitwardenHttpClient mockHttpForSecret(String secretKey, String secretValue) throws Exception {
- String encryptedKey = encrypt(secretKey);
- String encryptedValue = encrypt(secretValue);
- String secretId = "secret-id-001";
-
- BitwardenHttpClient client = mock(BitwardenHttpClient.class);
- when(client.post(anyString(), anyString())).thenReturn(BEARER_TOKEN_RESPONSE);
- when(client.get(contains("/organizations/"), eq("test-bearer")))
- .thenReturn("{\"data\":[{\"id\":\"" + secretId + "\",\"key\":\""
- + encryptedKey + "\"}]}");
- when(client.get(contains("/secrets/" + secretId), eq("test-bearer")))
- .thenReturn("{\"id\":\"" + secretId + "\",\"key\":\"" + encryptedKey
- + "\",\"value\":\"" + encryptedValue + "\"}");
- return client;
- }
-
- /** Produces a valid Bitwarden type-2 cipher string using the test key material. */
- private String encrypt(String plaintext) throws Exception {
- byte[] iv = new byte[16];
- new SecureRandom().nextBytes(iv);
-
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(TEST_ENC_KEY, "AES"), new IvParameterSpec(iv));
- byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
-
- Mac mac = Mac.getInstance("HmacSHA256");
- mac.init(new SecretKeySpec(TEST_MAC_KEY, "HmacSHA256"));
- mac.update(iv);
- byte[] hmac = mac.doFinal(ciphertext);
-
- Base64.Encoder b64 = Base64.getEncoder();
- return "2." + b64.encodeToString(iv) + "|"
- + b64.encodeToString(ciphertext) + "|"
- + b64.encodeToString(hmac);
- }
-}
diff --git a/gcp-secretmanager-secrets-provider/build.gradle b/gcp-secretmanager-secrets-provider/build.gradle
deleted file mode 100644
index 7980275c3..000000000
--- a/gcp-secretmanager-secrets-provider/build.gradle
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// Google Cloud Secret Manager Java client (Apache License 2.0)
-// https://github.com/googleapis/java-secretmanager
-dependencies {
- pluginLibsCompile 'com.google.cloud:google-cloud-secretmanager:2.36.0'
-}
-
-configurations.all {
- exclude group: 'commons-logging', module: 'commons-logging'
-}
diff --git a/gcp-secretmanager-secrets-provider/config/gcp-secret-manager.properties b/gcp-secretmanager-secrets-provider/config/gcp-secret-manager.properties
deleted file mode 100644
index ccff5dd36..000000000
--- a/gcp-secretmanager-secrets-provider/config/gcp-secret-manager.properties
+++ /dev/null
@@ -1,34 +0,0 @@
-###############################################################################
-# GCP Secret Manager — SecretProvider configuration
-#
-# To activate this provider:
-# 1. Set enabled="true" in gcp-secretmanager-secrets-provider/ofbiz-component.xml
-# 2. Set enabled="false" on every other *-secrets-provider plugin
-###############################################################################
-
-# GCP project ID that owns the secrets (required)
-# Example: my-gcp-project
-gcp.project.id=
-
-# Path to a service account JSON key file.
-# Leave empty to use Application Default Credentials (ADC) — recommended when
-# running on GCP (GKE, GCE, Cloud Run, App Engine) or with gcloud auth set up.
-gcp.credentials.file=
-
-# Optional prefix prepended to every secret name before lookup.
-# Useful for environment namespacing, e.g. "prod/" makes key "db" → "prod/db".
-gcp.secret.name.prefix=
-
-# GCP Secret Manager secret version to access (default: latest)
-gcp.secret.version=latest
-
-# GCP secret names may only contain letters, digits, hyphens and underscores.
-# OFBiz keys like "jdbc-password.mysql-ofbiz" contain a dot, which is invalid.
-# Set this to the replacement character (default: -) so the dot is substituted.
-# Example: "jdbc-password.mysql-ofbiz" → "jdbc-password-mysql-ofbiz"
-# Set to empty to disable replacement (only do this if your keys have no dots).
-gcp.secret.name.dot.replacement=-
-
-# In-memory cache TTL in seconds (default: 3600 = 1 hour).
-# Set to 0 to disable caching (fetches from GCP on every call).
-gcp.cache.ttl.seconds=3600
diff --git a/gcp-secretmanager-secrets-provider/ofbiz-component.xml b/gcp-secretmanager-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index 7608b2281..000000000
--- a/gcp-secretmanager-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="gcp-secretmanager-secrets-provider" enabled="false"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so gcp-secret-manager.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProvider.java b/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProvider.java
deleted file mode 100644
index 5a3e3a368..000000000
--- a/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProvider.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.gcpsecretmanager;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.concurrent.ConcurrentHashMap;
-
-import com.google.api.gax.core.FixedCredentialsProvider;
-import com.google.auth.oauth2.GoogleCredentials;
-import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
-import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-/**
- * {@link SecretProvider} implementation backed by Google Cloud Secret Manager.
- *
- * <h2>Authentication</h2>
- * <p>Two modes are supported via {@code gcp.credentials.file}:</p>
- * <ul>
- * <li><strong>Application Default Credentials (ADC)</strong> — leave the property empty.
- * GCP automatically provides credentials when running on GKE, GCE, Cloud Run, or
- * after {@code gcloud auth application-default login} locally.</li>
- * <li><strong>Service Account key file</strong> — set {@code gcp.credentials.file} to the
- * path of a downloaded JSON key file. Suitable for on-premise deployments.</li>
- * </ul>
- *
- * <h2>Secret name mapping</h2>
- * <p>GCP secret names may only contain letters, digits, hyphens and underscores.
- * OFBiz keys contain dots (e.g. {@code jdbc-password.mysql-ofbiz}), which are invalid.
- * Set {@code gcp.secret.name.dot.replacement=-} (the default) to replace dots with
- * hyphens, so the key maps to {@code jdbc-password-mysql-ofbiz} in the vault.</p>
- *
- * <h2>Secret resource name</h2>
- * <p>Built as:
- * {@code projects/{projectId}/secrets/{prefix}{sanitizedKey}/versions/{version}}</p>
- *
- * <p>Configure via {@code plugins/gcp-secretmanager-secrets-provider/config/gcp-secret-manager.properties}.</p>
- */
-@ThreadSafe
-public final class GcpSecretManagerSecretsProvider implements SecretProvider {
-
- private static final String MODULE = GcpSecretManagerSecretsProvider.class.getName();
- private static final String CONFIG_RESOURCE = "gcp-secret-manager";
-
- private final GcpSecretReader secretReader;
- private final String projectId;
- private final String secretNamePrefix;
- private final String version;
- private final String dotReplacement;
- private final long cacheTtlMs;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- final String value;
- final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public GcpSecretManagerSecretsProvider() throws GeneralException {
- this(readerFrom(buildClient()),
- prop("gcp.project.id", ""),
- prop("gcp.secret.name.prefix", ""),
- prop("gcp.secret.version", "latest"),
- prop("gcp.secret.name.dot.replacement", "-"),
- readTtlMs());
- }
-
- /** Package-private constructor used by unit tests to inject a {@link GcpSecretReader} lambda. */
- GcpSecretManagerSecretsProvider(GcpSecretReader secretReader, String projectId,
- String secretNamePrefix, String version, String dotReplacement, long cacheTtlMs) {
- this.secretReader = secretReader;
- this.projectId = projectId;
- this.secretNamePrefix = secretNamePrefix;
- this.version = version;
- this.dotReplacement = dotReplacement;
- this.cacheTtlMs = cacheTtlMs;
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.value;
- }
-
- String sanitizedKey = dotReplacement.isEmpty() ? key : key.replace(".", dotReplacement);
- String secretName = secretNamePrefix + sanitizedKey;
- String resourceName = "projects/" + projectId + "/secrets/" + secretName + "/versions/" + version;
-
- String value;
- try {
- value = secretReader.read(resourceName);
- } catch (Exception e) {
- throw new GeneralException(
- "GCP Secret Manager read failed for '" + resourceName + "': " + e.getMessage(), e);
- }
-
- if (value == null || value.isEmpty()) {
- throw new GeneralException(
- "GCP Secret Manager returned empty value for '" + resourceName + "'");
- }
-
- cache.put(key, new CacheEntry(value, cacheTtlMs));
- return value;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * to re-fetch from GCP. Useful after a secret rotation.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("GcpSecretManagerSecretsProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private static GcpSecretReader readerFrom(SecretManagerServiceClient client) {
- return resourceName -> {
- try {
- return client.accessSecretVersion(resourceName)
- .getPayload()
- .getData()
- .toStringUtf8();
- } catch (Exception e) {
- throw new IOException(e.getMessage(), e);
- }
- };
- }
-
- private static SecretManagerServiceClient buildClient() throws GeneralException {
- String credentialsFile = prop("gcp.credentials.file", "");
- try {
- SecretManagerServiceSettings.Builder builder = SecretManagerServiceSettings.newBuilder();
-
- if (!credentialsFile.isEmpty()) {
- try (FileInputStream fis = new FileInputStream(credentialsFile)) {
- GoogleCredentials credentials = GoogleCredentials
- .fromStream(fis)
- .createScoped("https://www.googleapis.com/auth/cloud-platform");
- builder.setCredentialsProvider(FixedCredentialsProvider.create(credentials));
- }
- }
- // else: Application Default Credentials are used automatically
-
- Debug.logInfo("GcpSecretManagerSecretsProvider: initialized project="
- + prop("gcp.project.id", "") + " version=" + prop("gcp.secret.version", "latest"), MODULE);
- return SecretManagerServiceClient.create(builder.build());
-
- } catch (IOException e) {
- throw new GeneralException(
- "GcpSecretManagerSecretsProvider: initialization failed: " + e.getMessage(), e);
- }
- }
-
- private static long readTtlMs() {
- String raw = prop("gcp.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid gcp.cache.ttl.seconds '" + raw + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-
- private static String prop(String key, String defaultValue) {
- return UtilProperties.getPropertyValue(CONFIG_RESOURCE, key, defaultValue);
- }
-}
diff --git a/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretReader.java b/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretReader.java
deleted file mode 100644
index 411cfaa7f..000000000
--- a/gcp-secretmanager-secrets-provider/src/main/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretReader.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.gcpsecretmanager;
-
-/**
- * Thin seam over GCP Secret Manager reads used by {@link GcpSecretManagerSecretsProvider}.
- * Kept package-private so tests can substitute a lambda without connecting to GCP.
- */
-@FunctionalInterface
-interface GcpSecretReader {
- /**
- * Accesses the secret version at the given fully-qualified resource name.
- *
- * @param resourceName e.g. {@code "projects/my-project/secrets/my-secret/versions/latest"}
- * @return the plaintext secret value; never {@code null}
- * @throws Exception if the access fails or the secret does not exist
- */
- String read(String resourceName) throws Exception;
-}
diff --git a/gcp-secretmanager-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/gcp-secretmanager-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 7421bf9dd..000000000
--- a/gcp-secretmanager-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.gcpsecretmanager.GcpSecretManagerSecretsProvider
diff --git a/gcp-secretmanager-secrets-provider/src/test/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProviderTest.java b/gcp-secretmanager-secrets-provider/src/test/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProviderTest.java
deleted file mode 100644
index 053fc3ddf..000000000
--- a/gcp-secretmanager-secrets-provider/src/test/java/org/apache/ofbiz/gcpsecretmanager/GcpSecretManagerSecretsProviderTest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.gcpsecretmanager;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.Test;
-
-public class GcpSecretManagerSecretsProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
- private static final String PROJECT = "my-project";
-
- // -- Happy path --
-
- @Test
- public void getSecret_returnsSecretValue() throws GeneralException {
- GcpSecretReader reader = fixedReader(
- "projects/my-project/secrets/mykey/versions/latest", "s3cr3t");
- assertEquals("s3cr3t", provider(reader, "", "-").getSecret("mykey"));
- }
-
- @Test
- public void getSecret_sanitizesDotInKey() throws GeneralException {
- // OFBiz key "jdbc-password.mysql-ofbiz" → GCP name "jdbc-password-mysql-ofbiz"
- GcpSecretReader reader = fixedReader(
- "projects/my-project/secrets/jdbc-password-mysql-ofbiz/versions/latest", "dbpass");
- assertEquals("dbpass", provider(reader, "", "-").getSecret("jdbc-password.mysql-ofbiz"));
- }
-
- @Test
- public void getSecret_appliesPrefixAfterSanitizing() throws GeneralException {
- GcpSecretReader reader = fixedReader(
- "projects/my-project/secrets/prod-jdbc-password-mydb/versions/latest", "prodpass");
- assertEquals("prodpass", provider(reader, "prod-", "-").getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_usesConfiguredVersion() throws GeneralException {
- GcpSecretReader reader = fixedReader(
- "projects/my-project/secrets/mykey/versions/3", "v3val");
- GcpSecretManagerSecretsProvider p = new GcpSecretManagerSecretsProvider(
- reader, PROJECT, "", "3", "-", ONE_HOUR_MS);
- assertEquals("v3val", p.getSecret("mykey"));
- }
-
- @Test
- public void getSecret_cachePreventsDuplicateReaderCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- GcpSecretReader reader = resourceName -> {
- calls.incrementAndGet();
- return "val";
- };
- GcpSecretManagerSecretsProvider p = provider(reader, "", "-");
-
- p.getSecret("mykey");
- p.getSecret("mykey"); // cache hit
-
- assertEquals(1, calls.get());
- }
-
- @Test
- public void invalidateCache_forcesRefetchOnNextCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- GcpSecretReader reader = resourceName -> {
- calls.incrementAndGet();
- return "val";
- };
- GcpSecretManagerSecretsProvider p = provider(reader, "", "-");
-
- p.getSecret("mykey");
- p.invalidateCache();
- p.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- @Test
- public void getSecret_expiredCacheTriggersRefetch() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- GcpSecretReader reader = resourceName -> {
- calls.incrementAndGet();
- return "val";
- };
- // TTL of -1 ms — entries expire immediately
- GcpSecretManagerSecretsProvider p = new GcpSecretManagerSecretsProvider(
- reader, PROJECT, "", "latest", "-", -1L);
-
- p.getSecret("mykey");
- p.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- @Test
- public void getSecret_dotReplacementDisabled_keepsDotsInName() throws GeneralException {
- // dot.replacement="" → no sanitization
- GcpSecretReader reader = fixedReader(
- "projects/my-project/secrets/my.key/versions/latest", "val");
- assertEquals("val", provider(reader, "", "").getSecret("my.key"));
- }
-
- // -- Error handling --
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnReaderException() throws GeneralException {
- GcpSecretReader reader = resourceName -> { throw new Exception("NOT_FOUND"); };
- provider(reader, "", "-").getSecret("missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnEmptyValue() throws GeneralException {
- GcpSecretReader reader = resourceName -> "";
- provider(reader, "", "-").getSecret("mykey");
- }
-
- // -- helpers --
-
- private static GcpSecretManagerSecretsProvider provider(GcpSecretReader reader,
- String prefix, String dotReplacement) {
- return new GcpSecretManagerSecretsProvider(reader, PROJECT, prefix, "latest",
- dotReplacement, ONE_HOUR_MS);
- }
-
- private static GcpSecretReader fixedReader(String expectedResource, String value) {
- return resourceName -> expectedResource.equals(resourceName) ? value : null;
- }
-}
diff --git a/hashicorp-vault-secrets-provider/build.gradle b/hashicorp-vault-secrets-provider/build.gradle
deleted file mode 100644
index ae4c90ea9..000000000
--- a/hashicorp-vault-secrets-provider/build.gradle
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// HashiCorp Vault Java Driver — Apache License 2.0
-// https://github.com/jopenlibs/vault-java-driver
-dependencies {
- pluginLibsCompile 'io.github.jopenlibs:vault-java-driver:5.4.0'
-}
-
-configurations.all {
- exclude group: 'commons-logging', module: 'commons-logging'
-}
diff --git a/hashicorp-vault-secrets-provider/config/hashicorp-vault-secrets.properties b/hashicorp-vault-secrets-provider/config/hashicorp-vault-secrets.properties
deleted file mode 100644
index 9829048e5..000000000
--- a/hashicorp-vault-secrets-provider/config/hashicorp-vault-secrets.properties
+++ /dev/null
@@ -1,66 +0,0 @@
-###############################################################################
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-###############################################################################
-
-####
-# HashiCorp Vault — SecretProvider configuration
-#
-# NOTE: Only ONE SecretProvider plugin may be active at a time.
-# If multiple provider plugins are on the classpath, the first one
-# discovered by java.util.ServiceLoader is used (order is undefined).
-# Deploy only the plugin that matches your environment.
-####
-
-# Vault server address.
-vault.address=http://127.0.0.1:8200
-
-# Authentication method: token | approle
-vault.auth.method=token
-
-# Token auth: set vault.token (leave empty when using AppRole).
-# Never commit real tokens — use an environment variable or a secrets manager
-# to inject this value at deploy time.
-vault.token=
-
-# AppRole auth: role_id is non-sensitive; secret_id should be injected at runtime.
-vault.approle.role_id=
-vault.approle.secret_id=
-
-# KV secrets engine mount path (the path you used when enabling the engine).
-vault.kv.mount=secret
-
-# KV engine version: 1 or 2 (Vault default is 2 for new mounts).
-vault.kv.version=2
-
-# Optional prefix prepended to every OFBiz secret key before the Vault lookup.
-# Example: with prefix "myapp/prod/" the key "jdbc-password.ofbiz" becomes
-# "secret/myapp/prod/jdbc-password.ofbiz" in Vault.
-vault.secret.name.prefix=
-
-# If the secret in Vault holds multiple fields (e.g. {"username":"u","password":"p"}),
-# set this to the field name that holds the actual secret value (e.g. "password").
-# Leave empty only when the secret has exactly one field — its value is returned as-is.
-vault.field=password
-
-# How long (in seconds) to cache a resolved secret value before re-fetching.
-# Default: 3600 (1 hour). Set to 0 to disable caching.
-vault.cache.ttl.seconds=3600
-
-# Whether to verify the Vault server's TLS certificate.
-# Set to false only for local development — never in production.
-vault.ssl.verify=true
diff --git a/hashicorp-vault-secrets-provider/ofbiz-component.xml b/hashicorp-vault-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index 37ea052c7..000000000
--- a/hashicorp-vault-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="hashicorp-vault-secrets-provider" enabled="false"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so hashicorp-vault-secrets.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultReader.java b/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultReader.java
deleted file mode 100644
index 0818488f5..000000000
--- a/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultReader.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.hashicorpvault;
-
-import java.util.Map;
-
-import io.github.jopenlibs.vault.VaultException;
-
-/**
- * Thin seam over Vault KV reads used by {@link HashicorpVaultSecretsProvider}.
- * Kept package-private so tests can substitute a lambda without launching a real Vault server.
- */
-@FunctionalInterface
-interface HashicorpVaultReader {
- /**
- * Reads the KV secret at the given path.
- *
- * @param path the full KV path (e.g. {@code "secret/myapp/jdbc-password"})
- * @return the data fields; never {@code null}
- * @throws VaultException if the read fails or the path does not exist
- */
- Map<String, String> read(String path) throws VaultException;
-}
diff --git a/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProvider.java b/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProvider.java
deleted file mode 100644
index ab16ca8e6..000000000
--- a/hashicorp-vault-secrets-provider/src/main/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProvider.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.hashicorpvault;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import java.util.Collections;
-
-import io.github.jopenlibs.vault.SslConfig;
-import io.github.jopenlibs.vault.Vault;
-import io.github.jopenlibs.vault.VaultConfig;
-import io.github.jopenlibs.vault.VaultException;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-/**
- * {@link SecretProvider} implementation backed by HashiCorp Vault.
- *
- * <p>Supports both Token and AppRole authentication methods. The preferred
- * production approach is AppRole — the role_id is non-sensitive and can live
- * in config, while the secret_id is injected at deploy time via an environment
- * variable or a secrets bootstrap mechanism.</p>
- *
- * <p>Both KV v1 and KV v2 engines are supported via the {@code vault.kv.version}
- * property. The driver handles the {@code /data/} path rewrite for KV v2
- * automatically when {@code engineVersion(2)} is configured.</p>
- *
- * <p>Resolved secret values are cached in memory for the TTL configured by
- * {@code vault.cache.ttl.seconds} (default 1 hour).</p>
- *
- * <p>Configure via {@code plugins/hashicorp-vault-secrets-provider/config/hashicorp-vault-secrets.properties}.</p>
- */
-@ThreadSafe
-public final class HashicorpVaultSecretsProvider implements SecretProvider {
-
- private static final String MODULE = HashicorpVaultSecretsProvider.class.getName();
- private static final String CONFIG_RESOURCE = "hashicorp-vault-secrets";
-
- private final HashicorpVaultReader vaultReader;
- private final String kvMount;
- private final String secretNamePrefix;
- private final String field;
- private final long cacheTtlMs;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- final String value;
- final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public HashicorpVaultSecretsProvider() {
- this(readerFrom(buildVault()),
- prop("vault.kv.mount", "secret"),
- prop("vault.secret.name.prefix", ""),
- prop("vault.field", "password"),
- readTtlMs());
- }
-
- /** Package-private constructor used by unit tests to inject a {@link HashicorpVaultReader} lambda. */
- HashicorpVaultSecretsProvider(HashicorpVaultReader vaultReader, String kvMount, String secretNamePrefix,
- String field, long cacheTtlMs) {
- this.vaultReader = vaultReader;
- this.kvMount = kvMount;
- this.secretNamePrefix = secretNamePrefix;
- this.field = field;
- this.cacheTtlMs = cacheTtlMs;
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.value;
- }
-
- String path = kvMount + "/" + secretNamePrefix + key;
- String value = readFromVault(path);
-
- cache.put(key, new CacheEntry(value, cacheTtlMs));
- return value;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * for each key to re-fetch from Vault. Useful after a secret rotation.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("HashicorpVaultSecretsProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private String readFromVault(String path) throws GeneralException {
- Map<String, String> data;
- try {
- data = vaultReader.read(path);
- } catch (VaultException e) {
- throw new GeneralException("Vault error reading path '" + path + "': " + e.getMessage(), e);
- }
-
- if (data == null || data.isEmpty()) {
- throw new GeneralException("No data found at Vault path '" + path + "' — check the path and auth policy");
- }
-
- if (!field.isEmpty()) {
- String value = data.get(field);
- if (value == null) {
- throw new GeneralException(
- "Field '" + field + "' not found in Vault secret at '" + path + "'");
- }
- return value;
- }
-
- if (data.size() == 1) {
- return data.values().iterator().next();
- }
-
- throw new GeneralException("Secret at '" + path + "' has " + data.size()
- + " fields — set vault.field to select one");
- }
-
- private static HashicorpVaultReader readerFrom(Vault vault) {
- return path -> {
- Map<String, String> data = vault.logical().read(path).getData();
- return data != null ? data : Collections.emptyMap();
- };
- }
-
- private static Vault buildVault() {
- String address = prop("vault.address", "http://127.0.0.1:8200");
- String authMethod = prop("vault.auth.method", "token");
- int kvVersion = parseKvVersion(prop("vault.kv.version", "2"));
- boolean sslVerify = Boolean.parseBoolean(prop("vault.ssl.verify", "true"));
-
- try {
- SslConfig ssl = new SslConfig().verify(sslVerify).build();
- String token;
-
- if ("approle".equalsIgnoreCase(authMethod)) {
- String roleId = prop("vault.approle.role_id", "");
- String secretId = prop("vault.approle.secret_id", "");
- VaultConfig bootConfig = new VaultConfig()
- .address(address)
- .engineVersion(kvVersion)
- .sslConfig(ssl)
- .build();
- token = new Vault(bootConfig)
- .auth()
- .loginByAppRole(roleId, secretId)
- .getAuthClientToken();
- } else {
- token = prop("vault.token", "");
- }
-
- VaultConfig config = new VaultConfig()
- .address(address)
- .token(token)
- .engineVersion(kvVersion)
- .sslConfig(ssl)
- .build();
-
- Debug.logInfo("HashicorpVaultSecretsProvider: initialized address=" + address
- + " auth=" + authMethod + " kv-version=" + kvVersion, MODULE);
- return new Vault(config);
-
- } catch (VaultException e) {
- throw new RuntimeException("HashicorpVaultSecretsProvider: initialization failed: " + e.getMessage(), e);
- }
- }
-
- private static int parseKvVersion(String raw) {
- try {
- int v = Integer.parseInt(raw.trim());
- if (v == 1 || v == 2) return v;
- } catch (NumberFormatException ignored) { }
- Debug.logWarning("Invalid vault.kv.version '" + raw + "', defaulting to 2", MODULE);
- return 2;
- }
-
- private static long readTtlMs() {
- String raw = prop("vault.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid vault.cache.ttl.seconds '" + raw + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-
- private static String prop(String key, String defaultValue) {
- return UtilProperties.getPropertyValue(CONFIG_RESOURCE, key, defaultValue);
- }
-}
diff --git a/hashicorp-vault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/hashicorp-vault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 2e36cff5e..000000000
--- a/hashicorp-vault-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.hashicorpvault.HashicorpVaultSecretsProvider
diff --git a/hashicorp-vault-secrets-provider/src/test/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProviderTest.java b/hashicorp-vault-secrets-provider/src/test/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProviderTest.java
deleted file mode 100644
index 22afb7cf9..000000000
--- a/hashicorp-vault-secrets-provider/src/test/java/org/apache/ofbiz/hashicorpvault/HashicorpVaultSecretsProviderTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.hashicorpvault;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import io.github.jopenlibs.vault.VaultException;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.Test;
-
-public class HashicorpVaultSecretsProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
-
- // -- Happy path --
-
- @Test
- public void getSecret_returnsSingleFieldSecret() throws GeneralException {
- HashicorpVaultReader reader = fixedReader("secret/mykey", singleEntry("value", "s3cr3t"));
- HashicorpVaultSecretsProvider provider = provider(reader, "", "");
-
- assertEquals("s3cr3t", provider.getSecret("mykey"));
- }
-
- @Test
- public void getSecret_extractsNamedField() throws GeneralException {
- HashicorpVaultReader reader = fixedReader("secret/mykey", twoEntry("username", "user", "password", "s3cr3t"));
- HashicorpVaultSecretsProvider provider = provider(reader, "", "password");
-
- assertEquals("s3cr3t", provider.getSecret("mykey"));
- }
-
- @Test
- public void getSecret_appliesKvMountAndPrefix() throws GeneralException {
- HashicorpVaultReader reader = fixedReader("kv/prod/jdbc-password.ofbiz", singleEntry("value", "dbpass"));
- HashicorpVaultSecretsProvider provider = new HashicorpVaultSecretsProvider(reader, "kv", "prod/", "", ONE_HOUR_MS);
-
- assertEquals("dbpass", provider.getSecret("jdbc-password.ofbiz"));
- }
-
- @Test
- public void getSecret_cachePreventsDuplicateReaderCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- HashicorpVaultReader reader = path -> {
- calls.incrementAndGet();
- return singleEntry("value", "s3cr3t");
- };
- HashicorpVaultSecretsProvider provider = provider(reader, "", "");
-
- provider.getSecret("mykey");
- provider.getSecret("mykey"); // second call — cache hit
-
- assertEquals(1, calls.get());
- }
-
- @Test
- public void invalidateCache_forcesRefetchOnNextCall() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- HashicorpVaultReader reader = path -> {
- calls.incrementAndGet();
- return singleEntry("value", "val");
- };
- HashicorpVaultSecretsProvider provider = provider(reader, "", "");
-
- provider.getSecret("mykey");
- provider.invalidateCache();
- provider.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- @Test
- public void getSecret_expiredCacheEntryTriggersRefetch() throws GeneralException {
- AtomicInteger calls = new AtomicInteger();
- HashicorpVaultReader reader = path -> {
- calls.incrementAndGet();
- return singleEntry("value", "val");
- };
- // TTL of -1 ms means entries expire immediately
- HashicorpVaultSecretsProvider provider = new HashicorpVaultSecretsProvider(reader, "secret", "", "", -1L);
-
- provider.getSecret("mykey");
- provider.getSecret("mykey");
-
- assertEquals(2, calls.get());
- }
-
- // -- Error handling --
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenDataIsEmpty() throws GeneralException {
- HashicorpVaultReader reader = path -> Collections.emptyMap();
- provider(reader, "", "").getSecret("missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenFieldMissing() throws GeneralException {
- HashicorpVaultReader reader = fixedReader("secret/mykey", singleEntry("username", "dbuser"));
- provider(reader, "", "password").getSecret("mykey"); // "password" field not present
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenMultiFieldAndNoFieldConfigured() throws GeneralException {
- HashicorpVaultReader reader = fixedReader("secret/mykey", twoEntry("username", "u", "password", "p"));
- provider(reader, "", "").getSecret("mykey"); // ambiguous — 2 fields, no field config
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnVaultException() throws GeneralException {
- HashicorpVaultReader reader = path -> { throw new VaultException("connection refused", 503); };
- provider(reader, "", "").getSecret("mykey");
- }
-
- // -- helpers --
-
- private static HashicorpVaultSecretsProvider provider(HashicorpVaultReader reader, String prefix, String field) {
- return new HashicorpVaultSecretsProvider(reader, "secret", prefix, field, ONE_HOUR_MS);
- }
-
- private static HashicorpVaultReader fixedReader(String expectedPath, Map<String, String> data) {
- return path -> expectedPath.equals(path) ? data : Collections.emptyMap();
- }
-
- private static Map<String, String> singleEntry(String k, String v) {
- return Collections.singletonMap(k, v);
- }
-
- private static Map<String, String> twoEntry(String k1, String v1, String k2, String v2) {
- Map<String, String> m = new HashMap<>();
- m.put(k1, v1);
- m.put(k2, v2);
- return m;
- }
-}
diff --git a/onepassword-secrets-provider/build.gradle b/onepassword-secrets-provider/build.gradle
deleted file mode 100644
index b2ca70694..000000000
--- a/onepassword-secrets-provider/build.gradle
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-// No external SDK required.
-// Uses Java's built-in java.net.http.HttpClient (Java 11+) and OFBiz's
-// bundled jackson-databind for JSON parsing.
-dependencies {}
diff --git a/onepassword-secrets-provider/config/onepassword.properties b/onepassword-secrets-provider/config/onepassword.properties
deleted file mode 100644
index 6a46799df..000000000
--- a/onepassword-secrets-provider/config/onepassword.properties
+++ /dev/null
@@ -1,56 +0,0 @@
-###############################################################################
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-###############################################################################
-
-####
-# 1Password Connect Server — SecretProvider configuration
-#
-# Requires a self-hosted 1Password Connect Server.
-# See: https://developer.1password.com/docs/connect/
-#
-# NOTE: Only ONE SecretProvider plugin may be active at a time.
-# Deploy only the plugin that matches your environment.
-####
-
-# URL of your 1Password Connect Server (no trailing slash).
-op.connect.url=http://localhost:8080
-
-# Connect Server access token.
-# Inject at deploy time — do not commit a real token.
-op.connect.token=
-
-# UUID of the vault to search for secrets.
-# Find it in the 1Password app or via: GET /v1/vaults
-op.vault.id=
-
-# The item field label whose value is returned as the secret.
-# Default is "password" — matches the standard Login and Password item templates.
-op.field=password
-
-# Optional prefix prepended to the OFBiz key when searching for the 1Password item title.
-# Example: with prefix "myapp/" the key "jdbc-password.ofbiz" matches an item titled
-# "myapp/jdbc-password.ofbiz" in 1Password.
-op.secret.name.prefix=
-
-# How long (in seconds) to cache a resolved secret value before re-fetching.
-# Default: 3600 (1 hour). Set to 0 to disable caching.
-op.cache.ttl.seconds=3600
-
-# HTTP connect and read timeouts in seconds.
-op.connect.timeout.seconds=5
-op.read.timeout.seconds=10
diff --git a/onepassword-secrets-provider/ofbiz-component.xml b/onepassword-secrets-provider/ofbiz-component.xml
deleted file mode 100644
index 10f7cafe5..000000000
--- a/onepassword-secrets-provider/ofbiz-component.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-
-<ofbiz-component name="onepassword-secrets-provider" enabled="false"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd">
-
- <resource-loader name="main" type="component"/>
-
- <!-- Puts config/ on the classpath so onepassword.properties is found by UtilProperties -->
- <classpath type="dir" location="config"/>
-
-</ofbiz-component>
diff --git a/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordHttpClient.java b/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordHttpClient.java
deleted file mode 100644
index fc91c2d9d..000000000
--- a/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordHttpClient.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.onepassword;
-
-import java.io.IOException;
-
-/**
- * Thin seam over HTTP GET calls used by {@link OnePasswordSecretsProvider}.
- * Kept package-private so tests can substitute a mock without launching a real
- * Connect Server.
- */
-@FunctionalInterface
-interface OnePasswordHttpClient {
- /**
- * Performs an authenticated GET request.
- *
- * @param url the full request URL
- * @param bearerToken the Connect Server access token
- * @return the response body as a UTF-8 string
- * @throws IOException if the request fails or the server returns a non-2xx status
- */
- String get(String url, String bearerToken) throws IOException;
-}
diff --git a/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProvider.java b/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProvider.java
deleted file mode 100644
index 88cddd79e..000000000
--- a/onepassword-secrets-provider/src/main/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProvider.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.onepassword;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URLEncoder;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.concurrent.ConcurrentHashMap;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.ofbiz.base.lang.ThreadSafe;
-import org.apache.ofbiz.base.secret.SecretProvider;
-import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.GeneralException;
-import org.apache.ofbiz.base.util.UtilProperties;
-
-/**
- * {@link SecretProvider} implementation backed by a 1Password Connect Server.
- *
- * <p>Looks up items in a designated 1Password vault by matching the item title
- * to the OFBiz secret key (with an optional prefix). The value of the configured
- * field (default {@code "password"}) is returned as the secret.</p>
- *
- * <p>Authentication uses a static Connect Server access token configured in
- * {@code op.connect.token}. Inject this token at deploy time — do not commit it.</p>
- *
- * <p>API flow per lookup:</p>
- * <ol>
- * <li>{@code GET /v1/vaults/{vaultId}/items?filter=title eq "{title}"} — find the item UUID</li>
- * <li>{@code GET /v1/vaults/{vaultId}/items/{itemId}} — fetch the full item with field values</li>
- * <li>Scan {@code fields[]} for the entry whose {@code label} matches {@code op.field}</li>
- * </ol>
- *
- * <p>Configure via {@code plugins/onepassword-secrets-provider/config/onepassword.properties}.</p>
- */
-@ThreadSafe
-public final class OnePasswordSecretsProvider implements SecretProvider {
-
- private static final String MODULE = OnePasswordSecretsProvider.class.getName();
- private static final String CONFIG_RESOURCE = "onepassword";
- private static final ObjectMapper JSON = new ObjectMapper();
-
- private final OnePasswordHttpClient httpClient;
- private final String connectUrl;
- private final String token;
- private final String vaultId;
- private final String field;
- private final String secretNamePrefix;
- private final long cacheTtlMs;
-
- private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
-
- private static final class CacheEntry {
- final String value;
- final long expiresAt;
-
- CacheEntry(String value, long ttlMs) {
- this.value = value;
- this.expiresAt = System.currentTimeMillis() + ttlMs;
- }
-
- boolean isExpired() {
- return System.currentTimeMillis() >= expiresAt;
- }
- }
-
- /** Public no-arg constructor required by {@link java.util.ServiceLoader}. */
- public OnePasswordSecretsProvider() {
- this(buildHttpClient(),
- prop("op.connect.url", "http://localhost:8080").replaceAll("/+$", ""),
- prop("op.connect.token", ""),
- prop("op.vault.id", ""),
- prop("op.field", "password"),
- prop("op.secret.name.prefix", ""),
- readTtlMs());
- }
-
- /** Package-private constructor used by unit tests to inject a mock HTTP client. */
- OnePasswordSecretsProvider(OnePasswordHttpClient httpClient, String connectUrl, String token,
- String vaultId, String field, String secretNamePrefix, long cacheTtlMs) {
- this.httpClient = httpClient;
- this.connectUrl = connectUrl;
- this.token = token;
- this.vaultId = vaultId;
- this.field = field;
- this.secretNamePrefix = secretNamePrefix;
- this.cacheTtlMs = cacheTtlMs;
- }
-
- @Override
- public String getSecret(String key) throws GeneralException {
- CacheEntry cached = cache.get(key);
- if (cached != null && !cached.isExpired()) {
- return cached.value;
- }
-
- String title = secretNamePrefix + key;
- String value = fetchFromConnect(title);
-
- cache.put(key, new CacheEntry(value, cacheTtlMs));
- return value;
- }
-
- /**
- * Clears the in-memory cache, forcing the next {@link #getSecret(String)} call
- * to re-fetch from the Connect Server. Useful after a secret update in 1Password.
- */
- public void invalidateCache() {
- cache.clear();
- Debug.logInfo("OnePasswordSecretsProvider: secret cache invalidated", MODULE);
- }
-
- // -- private helpers --
-
- private String fetchFromConnect(String title) throws GeneralException {
- String itemId = findItemId(title);
- return fetchFieldValue(itemId, title);
- }
-
- private String findItemId(String title) throws GeneralException {
- String filter = "title eq \"" + title + "\"";
- String encodedFilter;
- try {
- encodedFilter = URLEncoder.encode(filter, StandardCharsets.UTF_8);
- } catch (Exception e) {
- throw new GeneralException("Failed to encode filter for title '" + title + "'", e);
- }
-
- String searchUrl = connectUrl + "/v1/vaults/" + vaultId + "/items?filter=" + encodedFilter;
- String responseBody;
- try {
- responseBody = httpClient.get(searchUrl, token);
- } catch (IOException e) {
- throw new GeneralException("1Password Connect request failed: " + e.getMessage(), e);
- }
-
- try {
- JsonNode items = JSON.readTree(responseBody);
- if (!items.isArray() || items.size() == 0) {
- throw new GeneralException("No 1Password item found with title '" + title + "'");
- }
- JsonNode idNode = items.get(0).get("id");
- if (idNode == null || idNode.isNull()) {
- throw new GeneralException("1Password item for '" + title + "' has no id field");
- }
- return idNode.asText();
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Failed to parse 1Password item search response: " + e.getMessage(), e);
- }
- }
-
- private String fetchFieldValue(String itemId, String title) throws GeneralException {
- String itemUrl = connectUrl + "/v1/vaults/" + vaultId + "/items/" + itemId;
- String responseBody;
- try {
- responseBody = httpClient.get(itemUrl, token);
- } catch (IOException e) {
- throw new GeneralException("1Password Connect request failed for item '" + itemId + "': " + e.getMessage(), e);
- }
-
- try {
- JsonNode item = JSON.readTree(responseBody);
- JsonNode fields = item.get("fields");
- if (fields == null || !fields.isArray()) {
- throw new GeneralException("1Password item '" + title + "' has no fields array");
- }
-
- for (JsonNode f : fields) {
- JsonNode labelNode = f.get("label");
- if (labelNode != null && field.equalsIgnoreCase(labelNode.asText())) {
- JsonNode valueNode = f.get("value");
- if (valueNode == null || valueNode.isNull()) {
- throw new GeneralException(
- "Field '" + field + "' in 1Password item '" + title + "' has a null value");
- }
- String value = valueNode.asText();
- if (value.isEmpty()) {
- throw new GeneralException(
- "Field '" + field + "' in 1Password item '" + title + "' is empty");
- }
- return value;
- }
- }
- throw new GeneralException(
- "Field '" + field + "' not found in 1Password item '" + title + "'");
- } catch (GeneralException e) {
- throw e;
- } catch (Exception e) {
- throw new GeneralException("Failed to parse 1Password item response: " + e.getMessage(), e);
- }
- }
-
- private static OnePasswordHttpClient buildHttpClient() {
- int connectTimeout = parseSeconds(prop("op.connect.timeout.seconds", "5"));
- int readTimeout = parseSeconds(prop("op.read.timeout.seconds", "10"));
-
- HttpClient javaClient = HttpClient.newBuilder()
- .connectTimeout(Duration.ofSeconds(connectTimeout))
- .build();
-
- Debug.logInfo("OnePasswordSecretsProvider: initialized connect-url=" + prop("op.connect.url", ""),
- MODULE);
-
- return (url, bearerToken) -> {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .header("Authorization", "Bearer " + bearerToken)
- .header("Accept", "application/json")
- .timeout(Duration.ofSeconds(readTimeout))
- .GET()
- .build();
-
- HttpResponse<String> response;
- try {
- response = javaClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("HTTP request interrupted", e);
- }
-
- int status = response.statusCode();
- if (status < 200 || status >= 300) {
- throw new IOException("1Password Connect returned HTTP " + status + " for " + url);
- }
- return response.body();
- };
- }
-
- private static int parseSeconds(String raw) {
- try {
- return Integer.parseInt(raw.trim());
- } catch (NumberFormatException e) {
- return 10;
- }
- }
-
- private static long readTtlMs() {
- String raw = prop("op.cache.ttl.seconds", "3600");
- try {
- return Long.parseLong(raw.trim()) * 1000L;
- } catch (NumberFormatException e) {
- Debug.logWarning("Invalid op.cache.ttl.seconds '" + raw + "', defaulting to 3600s", MODULE);
- return 3_600_000L;
- }
- }
-
- private static String prop(String key, String defaultValue) {
- return UtilProperties.getPropertyValue(CONFIG_RESOURCE, key, defaultValue);
- }
-}
diff --git a/onepassword-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider b/onepassword-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
deleted file mode 100644
index 128ee9165..000000000
--- a/onepassword-secrets-provider/src/main/resources/META-INF/services/org.apache.ofbiz.base.secret.SecretProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.ofbiz.onepassword.OnePasswordSecretsProvider
diff --git a/onepassword-secrets-provider/src/test/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProviderTest.java b/onepassword-secrets-provider/src/test/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProviderTest.java
deleted file mode 100644
index 1cb1dc519..000000000
--- a/onepassword-secrets-provider/src/test/java/org/apache/ofbiz/onepassword/OnePasswordSecretsProviderTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*******************************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- *******************************************************************************/
-package org.apache.ofbiz.onepassword;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-
-import org.apache.ofbiz.base.util.GeneralException;
-import org.junit.Test;
-
-public class OnePasswordSecretsProviderTest {
-
- private static final long ONE_HOUR_MS = 3_600_000L;
- private static final String BASE_URL = "http://localhost:8080";
- private static final String TOKEN = "test-token";
- private static final String VAULT_ID = "vault-abc";
- private static final String ITEM_ID = "item-1";
-
- // -- Happy path --
-
- @Test
- public void getSecret_returnsFieldValue() throws Exception {
- OnePasswordHttpClient client = buildMockClient("jdbc-password.mydb", ITEM_ID, "s3cr3t");
- assertEquals("s3cr3t", provider(client, "password", "").getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_appliesSecretNamePrefix() throws Exception {
- OnePasswordHttpClient client = buildMockClient("prod/jdbc-password.mydb", ITEM_ID, "dbpass");
- assertEquals("dbpass", provider(client, "password", "prod/").getSecret("jdbc-password.mydb"));
- }
-
- @Test
- public void getSecret_cachePreventsSecondHttpCall() throws Exception {
- OnePasswordHttpClient client = buildMockClient("mykey", ITEM_ID, "val");
- OnePasswordSecretsProvider p = provider(client, "password", "");
-
- p.getSecret("mykey");
- p.getSecret("mykey"); // cache hit — no HTTP calls
-
- // First getSecret = 2 calls (1 search + 1 fetch); second = 0 (cache)
- verify(client, times(2)).get(anyString(), anyString());
- }
-
- @Test
- public void invalidateCache_forcesRefetch() throws Exception {
- OnePasswordHttpClient client = buildMockClient("mykey", ITEM_ID, "val");
- OnePasswordSecretsProvider p = provider(client, "password", "");
-
- p.getSecret("mykey");
- p.invalidateCache();
- p.getSecret("mykey"); // cache cleared — full re-fetch
-
- // Two full fetches × 2 HTTP calls each = 4 total
- verify(client, times(4)).get(anyString(), anyString());
- }
-
- @Test
- public void getSecret_expiredCacheEntryTriggersRefetch() throws Exception {
- OnePasswordHttpClient client = buildMockClient("mykey", ITEM_ID, "val");
- OnePasswordSecretsProvider p = provider(client, "password", "", -1L); // instant expiry
-
- p.getSecret("mykey");
- p.getSecret("mykey"); // expired — must re-fetch
-
- verify(client, times(4)).get(anyString(), anyString());
- }
-
- // -- Error handling --
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenItemNotFound() throws Exception {
- OnePasswordHttpClient client = mock(OnePasswordHttpClient.class);
- when(client.get(anyString(), anyString())).thenReturn("[]");
-
- provider(client, "password", "").getSecret("missing");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsWhenFieldNotPresent() throws Exception {
- // Item exists but has no "password" field — only "username"
- OnePasswordHttpClient client = buildMockClient("mykey", ITEM_ID, "username", "dbuser", "password");
- provider(client, "password", "").getSecret("mykey");
- }
-
- @Test(expected = GeneralException.class)
- public void getSecret_throwsOnHttpError() throws Exception {
- OnePasswordHttpClient client = mock(OnePasswordHttpClient.class);
- when(client.get(anyString(), anyString())).thenThrow(new IOException("connection refused"));
-
- provider(client, "password", "").getSecret("mykey");
- }
-
- // -- helpers --
-
- private static OnePasswordSecretsProvider provider(OnePasswordHttpClient client, String field, String prefix) {
- return provider(client, field, prefix, ONE_HOUR_MS);
- }
-
- private static OnePasswordSecretsProvider provider(OnePasswordHttpClient client, String field,
- String prefix, long ttlMs) {
- return new OnePasswordSecretsProvider(client, BASE_URL, TOKEN, VAULT_ID, field, prefix, ttlMs);
- }
-
- /**
- * Builds a mock that routes by URL: search requests get the item list,
- * item-fetch requests get the full item with a "password" field.
- */
- private static OnePasswordHttpClient buildMockClient(String title, String itemId, String fieldValue)
- throws IOException {
- return buildMockClient(title, itemId, "password", fieldValue, "password");
- }
-
- /**
- * Builds a mock that exposes an item with the given fieldLabel/fieldValue,
- * regardless of which field the provider is configured to look for.
- */
- private static OnePasswordHttpClient buildMockClient(String title, String itemId,
- String fieldLabel, String fieldValue, String ignoredField) throws IOException {
- OnePasswordHttpClient client = mock(OnePasswordHttpClient.class);
- when(client.get(anyString(), eq(TOKEN))).thenAnswer(inv -> {
- String url = inv.getArgument(0, String.class);
- if (url.contains("filter=")) {
- return "[{\"id\":\"" + itemId + "\",\"title\":\"" + title + "\"}]";
- }
- // item detail
- return "{\"id\":\"" + itemId + "\",\"fields\":["
- + "{\"label\":\"" + fieldLabel + "\",\"value\":\"" + fieldValue + "\"}"
- + "]}";
- });
- return client;
- }
-}