diff --git a/sdk-java/.gitignore b/sdk-java/.gitignore new file mode 100644 index 00000000..7c8f575c --- /dev/null +++ b/sdk-java/.gitignore @@ -0,0 +1,42 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +dependency-reduced-pom.xml + +.idea \ No newline at end of file diff --git a/sdk-java/README.md b/sdk-java/README.md new file mode 100644 index 00000000..873ff0b1 --- /dev/null +++ b/sdk-java/README.md @@ -0,0 +1,30 @@ +# How to implement to your project + +``` + + org.wsecm + sdk-java + 1.0.0 + +``` + +``` + public void start() { + LOGGER.info("START...."); + String fetch = fetch("https://vsecm-safe.vsecm-system.svc.cluster.local:8443/sentinel/v1/secrets?reveal=true"); + LOGGER.info("RESULT .... " + fetch); + TaskFactory taskFactory = new FetchSecretsTaskFactory(); + Future watchedData = watch(taskFactory, "https://vsecm-safe.vsecm-system.svc.cluster.local:8443/sentinel/v1/secrets?reveal=true"); + if (watchedData.isDone()) { + try { + saveData(watchedData.get(), "/opt/vsecm/secrets.json"); + } catch (InterruptedException | ExecutionException e) { + LOGGER.info("ERROR .... " + e.getMessage()); + } + } + } +``` + +# TEST INSTRUCTIONS +- If you want to test properly you should revert changes to this **[commit](https://github.com/vmware-tanzu/secrets-manager/pull/732/commits/caa16943b6e2aeac714486b5e73b2aa13f1515c1)** +- Check README \ No newline at end of file diff --git a/sdk-java/pom.xml b/sdk-java/pom.xml new file mode 100644 index 00000000..59b7c23b --- /dev/null +++ b/sdk-java/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + org.vsecm + sdk-java + 1.0.0 + sdk-java + + + 11 + 11 + + + + + io.spiffe + java-spiffe-provider + 0.8.6 + + + io.spiffe + grpc-netty-linux + 0.8.6 + runtime + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 11 + + + + + diff --git a/sdk-java/src/main/java/org/vsecm/client/VSecMHttpClient.java b/sdk-java/src/main/java/org/vsecm/client/VSecMHttpClient.java new file mode 100644 index 00000000..83c239e4 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/client/VSecMHttpClient.java @@ -0,0 +1,76 @@ +/** + * Provides an HTTP client configured with SPIFFE based SSL context for secure communication. + * This client is specifically designed for environments that utilize SPIFFE for workload identification + * and secure communication. + * + *

This implementation leverages the SPIFFE Workload API to fetch X.509 SVIDs for establishing TLS connections, + * ensuring that the communication is both secure and aligned with the SPIFFE standards.

+ */ +package org.vsecm.client; + +import io.spiffe.exception.SocketEndpointAddressException; +import io.spiffe.exception.X509SourceException; +import io.spiffe.provider.SpiffeSslContextFactory; +import io.spiffe.workloadapi.DefaultX509Source; + +import javax.net.ssl.SSLContext; +import java.net.http.HttpClient; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a HTTP client that is secured with SPIFFE-based mutual TLS, + * utilizing the SPIFFE Workload API for fetching SVIDs. + */ +public class VSecMHttpClient { + private static final Logger LOGGER = Logger.getLogger(VSecMHttpClient.class.getName()); + private static final String SPIFFE_SOCKET_PATH = "unix:///spire-agent-socket/agent.sock"; + + /** + * Creates an instance of {@link HttpClient} with SPIFFE-based SSL context. + * This client can be used for secure HTTP communication. + * + * @return A configured {@link HttpClient} instance ready for secure communication. + * @throws RuntimeException if there's an issue configuring the SSL context, + * encapsulating any underlying exceptions. + */ + public HttpClient client() { + try { + SSLContext sslContext = configureSSLContext(); + return HttpClient.newBuilder().sslContext(sslContext).build(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to fetch secrets", e); + throw new RuntimeException(e); + } + } + + /** + * Configures and returns an {@link SSLContext} suitable for SPIFFE based secure communication. + * + * @return An {@link SSLContext} configured with SPIFFE X.509 SVIDs for mutual TLS. + * @throws SocketEndpointAddressException If the SPIFFE Workload API socket endpoint address is incorrect. + * @throws X509SourceException If there's an issue fetching or processing the X.509 SVIDs. + * @throws NoSuchAlgorithmException If the SSL context cannot be instantiated due to a missing algorithm. + * @throws KeyManagementException If there's an issue initializing the {@link SSLContext} with SPIFFE SVIDs. + */ + private SSLContext configureSSLContext() throws SocketEndpointAddressException, X509SourceException, NoSuchAlgorithmException, KeyManagementException { + DefaultX509Source.X509SourceOptions sourceOptions = DefaultX509Source.X509SourceOptions + .builder() + .spiffeSocketPath(SPIFFE_SOCKET_PATH) + .build(); + + DefaultX509Source x509Source = DefaultX509Source.newSource(sourceOptions); + LOGGER.info("SPIFFE_ID: " + x509Source.getX509Svid().getSpiffeId()); + + SpiffeSslContextFactory.SslContextOptions sslContextOptions = SpiffeSslContextFactory.SslContextOptions + .builder() + .acceptAnySpiffeId() + .x509Source(x509Source) + .build(); + + return SpiffeSslContextFactory.getSslContext(sslContextOptions); + } + +} \ No newline at end of file diff --git a/sdk-java/src/main/java/org/vsecm/service/FetchService.java b/sdk-java/src/main/java/org/vsecm/service/FetchService.java new file mode 100644 index 00000000..88dc8c81 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/service/FetchService.java @@ -0,0 +1,47 @@ +/** + * A service class for fetching data from specified URIs using a SPIFFE-enabled HTTP client. + * This service leverages {@link org.vsecm.client.VSecMHttpClient} for secure HTTP requests, + * ensuring communication adheres to the SPIFFE standard for secure and authenticated connections. + */ +package org.vsecm.service; + +import org.vsecm.client.VSecMHttpClient; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides static utility methods for fetching data from a specified URI. + * This class cannot be instantiated and serves purely as a utility class. + */ +public final class FetchService { + private static final Logger LOGGER = Logger.getLogger(FetchService.class.getName()); + + private FetchService(){} + + /** + * Fetches data as a {@link String} from the specified URI using an HTTP GET request. + * This method utilizes a SPIFFE-enabled HTTP client for secure communication. + * + * @param secretUri The URI from which to fetch data. Must be a valid URI string. + * @return The body of the HTTP response as a {@link String}. Returns an empty string if the request fails. + */ + public static String fetch(String secretUri) { + try { + HttpRequest request = HttpRequest.newBuilder(new URI(secretUri)).GET().build(); + VSecMHttpClient vSecMHttpClient = new VSecMHttpClient(); + HttpResponse response = vSecMHttpClient.client().send(request, HttpResponse.BodyHandlers.ofString()); + LOGGER.info("Response status code: " + response.statusCode()); + LOGGER.info("Response body: " + response.body()); + return response.body(); + } catch (URISyntaxException | IOException | InterruptedException e) { + LOGGER.log(Level.SEVERE, "Failed processes when request to URI : " + secretUri, e); + return ""; + } + } +} diff --git a/sdk-java/src/main/java/org/vsecm/service/SaveToFileService.java b/sdk-java/src/main/java/org/vsecm/service/SaveToFileService.java new file mode 100644 index 00000000..9350cb31 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/service/SaveToFileService.java @@ -0,0 +1,48 @@ +/** + * Provides functionality to save data to a file, creating the necessary directories if they do not exist. + * This service is designed to facilitate the saving of data to the file system, ensuring that the file + * path's parent directories are created if necessary. + */ +package org.vsecm.service; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility class for saving data to a file. It ensures that the parent directories of the specified + * file path are created before writing the data. This class cannot be instantiated. + */ +public class SaveToFileService { + private static final Logger LOGGER = Logger.getLogger(SaveToFileService.class.getName()); + + /** + * Saves the provided data to a file at the specified path. If the file's parent directories do not exist, + * they are created. If writing to the file fails, an error is logged. + * + * @param data The string data to be saved to the file. + * @param pathStr The file system path where the data should be saved. Must be a valid path string. + */ + public static void saveData(String data, String pathStr) { + Path path = Paths.get(pathStr); + + // Attempt to create the directory(ies) if they don't exist + try { + Files.createDirectories(path.getParent()); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to create directory for secrets", e); + return; + } + + // Attempt to write the data to the file + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(data); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error saving data to " + pathStr, e); + } + } +} diff --git a/sdk-java/src/main/java/org/vsecm/service/WatchService.java b/sdk-java/src/main/java/org/vsecm/service/WatchService.java new file mode 100644 index 00000000..e81aee03 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/service/WatchService.java @@ -0,0 +1,38 @@ +/** + * Provides a scheduling service for executing tasks that monitor or interact with specified resources, such as URIs. + * Utilizes a {@link TaskFactory} to create tasks that can be scheduled for execution. + * This service is designed to be flexible, allowing for different types of tasks to be executed based on the implementation + * provided by the {@link TaskFactory}. + */ +package org.vsecm.service; + +import org.vsecm.task.Task; +import org.vsecm.task.TaskFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; + +/** + * A utility class for scheduling and executing tasks. This class cannot be instantiated and is used + * to schedule tasks provided by a {@link TaskFactory}. The tasks can perform any operations defined + * by their implementation, such as monitoring changes to a resource or processing data. + */ +public final class WatchService { + private WatchService() {} + + /** + * Schedules and executes a task created by the provided {@link TaskFactory}. The task is executed + * using a single-threaded {@link ScheduledExecutorService}. + * + * @param taskFactory The factory to create a task for execution. + * @param secretUri The URI or resource identifier that the task will operate on or monitor. + * @return A {@link Future} representing pending completion of the task. The future's get method will + * return the task's result upon completion. + */ + public static Future watch(TaskFactory taskFactory, String secretUri) { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + Task task = taskFactory.createTask(scheduler); + return task.execute(secretUri); + } +} \ No newline at end of file diff --git a/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTask.java b/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTask.java new file mode 100644 index 00000000..2f8b6ec9 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTask.java @@ -0,0 +1,65 @@ +package org.vsecm.task; + +import org.vsecm.task.backoff.BackoffStrategy; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.vsecm.service.FetchService.fetch; + +/** + * A task for fetching secrets from a specified URI, with retry logic based on a backoff strategy. + * This task is designed to periodically fetch data, adjusting its retry interval based on the success + * and failure outcomes of each attempt. + */ +public class FetchSecretsTask implements Task { + + private static final Logger LOGGER = Logger.getLogger(FetchSecretsTask.class.getName()); + private final ScheduledExecutorService scheduler; + private final BackoffStrategy backoffStrategy; + private long interval; + private long successCount = 0; + private long errorCount = 0; + + /** + * Constructs a new {@code FetchSecretsTask} with the specified scheduler, backoff strategy, and initial interval. + * + * @param scheduler The {@link ScheduledExecutorService} to schedule task execution. + * @param backoffStrategy The {@link BackoffStrategy} to determine the interval between retries. + * @param initialInterval The initial interval (in milliseconds) before the first execution or retry. + */ + public FetchSecretsTask(ScheduledExecutorService scheduler, BackoffStrategy backoffStrategy, long initialInterval) { + this.scheduler = scheduler; + this.backoffStrategy = backoffStrategy; + this.interval = initialInterval; + } + + /** + * Schedules and executes the task of fetching secrets from the provided URI. If the fetch fails, + * the task will retry based on the backoff strategy provided during construction. + * + * @param secretUri The URI from which to fetch secrets. + * @return A {@link Future} representing pending completion of the task, containing the fetched data as a {@link String} + * upon success, or {@code null} if an error occurs. + */ + @Override + public Future execute(String secretUri) { + Callable task = () -> { + try { + String result = fetch(secretUri); + successCount++; + return result; + } catch (Exception e) { + errorCount++; + interval = backoffStrategy.calculateBackoff(interval, successCount, errorCount); + LOGGER.log(Level.WARNING, "Could not fetch secrets. Will retry in " + interval + "ms.", e); + return null; + } + }; + return scheduler.schedule(task, interval, TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTaskFactory.java b/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTaskFactory.java new file mode 100644 index 00000000..84319b14 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/FetchSecretsTaskFactory.java @@ -0,0 +1,29 @@ +package org.vsecm.task; + +import org.vsecm.task.backoff.BackoffStrategy; +import org.vsecm.task.backoff.ExponentialBackoffStrategy; + +import java.util.concurrent.ScheduledExecutorService; + +/** + * A factory for creating {@link FetchSecretsTask} instances. This factory configures the tasks + * with an {@link ExponentialBackoffStrategy} and a predefined initial interval, encapsulating + * the creation logic and dependencies of {@code FetchSecretsTask}. + */ +public class FetchSecretsTaskFactory implements TaskFactory { + + /** + * Creates and returns a new instance of {@link FetchSecretsTask}, configured with + * an {@link ExponentialBackoffStrategy} for retry logic and a predefined initial interval. + * This method provides a convenient way to instantiate {@code FetchSecretsTask} with + * recommended settings. + * + * @param scheduler The {@link ScheduledExecutorService} to be used by the created task for scheduling. + * @return A new instance of {@link FetchSecretsTask}, ready to be executed. + */ + @Override + public Task createTask(ScheduledExecutorService scheduler) { + BackoffStrategy backoffStrategy = new ExponentialBackoffStrategy(); + return new FetchSecretsTask(scheduler, backoffStrategy, 20000); + } +} \ No newline at end of file diff --git a/sdk-java/src/main/java/org/vsecm/task/Task.java b/sdk-java/src/main/java/org/vsecm/task/Task.java new file mode 100644 index 00000000..f3a744cd --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/Task.java @@ -0,0 +1,23 @@ +package org.vsecm.task; + +import java.util.concurrent.Future; + +/** + * Represents a general task that can be executed, typically involving asynchronous operations. + * This interface defines a single method for executing a task, which is intended to perform + * operations such as fetching or processing data from a specified URI and returning a future result. + */ +public interface Task { + /** + * Executes the task using the provided URI and returns a {@link Future} containing the result. + * The implementation of this method should define the specific operation to be performed, + * such as fetching data from the given URI. The method is designed to be asynchronous, allowing + * the calling thread to continue execution while the task is processed in the background. + * + * @param secretUri The URI or resource identifier that the task will operate on or fetch data from. + * @return A {@link Future} representing pending completion of the task. The Future's {@code get} method + * will return the task's result as a {@link String} upon successful completion, or {@code null} + * if the operation fails or does not produce a result. + */ + Future execute(String secretUri); +} \ No newline at end of file diff --git a/sdk-java/src/main/java/org/vsecm/task/TaskFactory.java b/sdk-java/src/main/java/org/vsecm/task/TaskFactory.java new file mode 100644 index 00000000..e1951f70 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/TaskFactory.java @@ -0,0 +1,24 @@ +package org.vsecm.task; + +import java.util.concurrent.ScheduledExecutorService; + +/** + * Defines a factory interface for creating instances of {@link Task}. This interface ensures + * that implementing classes provide a method to instantiate tasks, facilitating the creation + * of tasks with specific configurations or dependencies, such as a {@link ScheduledExecutorService}. + */ +public interface TaskFactory { + + /** + * Creates and returns a new {@link Task} instance, configured with the provided + * {@link ScheduledExecutorService}. This method allows for the dynamic creation of tasks, + * providing flexibility in how tasks are instantiated and scheduled. + * + * @param scheduler The {@link ScheduledExecutorService} that the created task will use + * for scheduling its execution. This service controls the timing and + * concurrency of the task execution. + * @return A new instance of a {@link Task}, ready to be executed with the given scheduler. + */ + Task createTask(ScheduledExecutorService scheduler); +} + diff --git a/sdk-java/src/main/java/org/vsecm/task/backoff/BackoffStrategy.java b/sdk-java/src/main/java/org/vsecm/task/backoff/BackoffStrategy.java new file mode 100644 index 00000000..46f48f0b --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/backoff/BackoffStrategy.java @@ -0,0 +1,20 @@ +package org.vsecm.task.backoff; + +/** + * Defines a strategy for calculating backoff intervals. Implementations of this interface + * can be used to determine the delay before retrying an operation, typically after a failure. + */ +public interface BackoffStrategy { + + /** + * Calculates the backoff interval based on the number of successes, the number of errors, + * and the current interval. + * + * @param interval The base interval or the current interval before applying the backoff strategy. + * @param successCount The number of successful operations. + * @param errorCount The number of failed operations. + * @return The calculated backoff interval. + */ + long calculateBackoff(long interval, long successCount, long errorCount); +} + diff --git a/sdk-java/src/main/java/org/vsecm/task/backoff/ExponentialBackoffStrategy.java b/sdk-java/src/main/java/org/vsecm/task/backoff/ExponentialBackoffStrategy.java new file mode 100644 index 00000000..b83ae457 --- /dev/null +++ b/sdk-java/src/main/java/org/vsecm/task/backoff/ExponentialBackoffStrategy.java @@ -0,0 +1,32 @@ +package org.vsecm.task.backoff; + +import static java.lang.Math.abs; + +/** + * Implements an exponential backoff strategy that increases the backoff interval based on the difference + * between the number of successes and errors. If the error count is greater than the success count, the interval + * is doubled to allow more time before the next retry. Otherwise, the interval remains unchanged. + */ +public class ExponentialBackoffStrategy implements BackoffStrategy { + + /** + * Calculates the backoff interval using an exponential strategy. The interval is doubled when + * the number of errors exceeds the number of successes. + * + * @param interval The base interval or the current interval before applying the backoff strategy. + * @param successCount The number of successful operations. + * @param errorCount The number of failed operations. + * @return The calculated backoff interval. Doubles the interval if errors exceed successes, otherwise + * returns the original interval. + */ + @Override + public long calculateBackoff(long interval, long successCount, long errorCount) { + long delta = abs(successCount - errorCount); + + if (delta > successCount){ + return interval * 2; + } + return interval; + } +} + diff --git a/sdk-java/src/main/resources/log4j.xml b/sdk-java/src/main/resources/log4j.xml new file mode 100644 index 00000000..1a24c00a --- /dev/null +++ b/sdk-java/src/main/resources/log4j.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file