diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/DistributedSecretsVaultApplication.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/DistributedSecretsVaultApplication.java index 546a54e..b51f20c 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/DistributedSecretsVaultApplication.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/DistributedSecretsVaultApplication.java @@ -5,11 +5,34 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.scheduling.annotation.EnableScheduling; +/** + * Entry point for the Distributed Secrets Vault application. + *

+ * This Spring Boot application provides a distributed, fault-tolerant secrets + * management system that uses Shamir's Secret Sharing to split secrets + * into shards distributed across multiple nodes. It relies on a two-phase + * prepare/commit protocol coordinated through Kafka, with ScaleCube for + * service discovery and Redis for shard storage. + *

+ * Key annotations: + *

+ */ @SpringBootApplication @ConfigurationPropertiesScan @EnableScheduling public class DistributedSecretsVaultApplication { + /** + * Launches the Spring Boot application. + * + * @param args command-line arguments forwarded to Spring Boot + */ public static void main(String[] args) { SpringApplication.run(DistributedSecretsVaultApplication.class, args); } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ClusterConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ClusterConfig.java index cf80ac0..01b2269 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ClusterConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ClusterConfig.java @@ -5,15 +5,37 @@ import lombok.Data; +/** + * Configuration properties for cluster topology and distributed operation tuning. + *

+ * Bound from the {@code cluster.*} prefix in {@code application.yml}. + * These values control how secrets are split, how many nodes must acknowledge + * a write, and how long pending operations remain valid. + * + * @see edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer + */ @Validated @ConfigurationProperties(prefix = "cluster") @Data public class ClusterConfig { + /** Total number of nodes in the cluster (N in Shamir's scheme). */ private int totalNodes; + + /** Minimum number of shards required to reconstruct a secret (K in Shamir's scheme). */ private int thresholdK; + + /** Minimum number of peer ACKs required to commit a write (quorum M). */ private int quorumM; + + /** Maximum time (ms) a pending action remains buffered before eviction. */ private long lockTimeoutMillis; + + /** Maximum time (ms) to wait for all peer responses during a write operation. */ private long writeTimeoutMillis; + + /** Whether read-repair is enabled (automatically re-split when shard count is low). */ private boolean repairEnabled = true; + + /** Number of extra shards above the threshold before read-repair triggers. */ private int repairTriggerBuffer = 1; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/KafkaConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/KafkaConfig.java index 14eaac8..3976df6 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/KafkaConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/KafkaConfig.java @@ -5,12 +5,31 @@ import org.springframework.context.annotation.Configuration; import org.springframework.kafka.config.TopicBuilder; +/** + * Kafka topic configuration for the distributed commit protocol. + *

+ * Defines the two Kafka topics used by the system: + *

+ * Both topics are configured with a 4-hour retention period. + */ @Configuration public class KafkaConfig { + /** Topic name for general coordination messages between nodes. */ public static final String COORDINATION_TOPIC = "secrets-coordination"; + + /** Topic name for commit messages broadcast after quorum is reached. */ public static final String COMMIT_TOPIC = "secrets-commit"; + /** + * Creates the coordination Kafka topic if it does not already exist. + * + * @return a {@link NewTopic} with 1 partition, 1 replica, and 4-hour retention + */ @Bean public NewTopic coordinationTopic() { return TopicBuilder.name(COORDINATION_TOPIC) @@ -20,6 +39,11 @@ public NewTopic coordinationTopic() { .build(); } + /** + * Creates the commit Kafka topic if it does not already exist. + * + * @return a {@link NewTopic} with 1 partition, 1 replica, and 4-hour retention + */ @Bean public NewTopic commitTopic() { return TopicBuilder.name(COMMIT_TOPIC) diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/NetworkConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/NetworkConfig.java index 9a45723..f4f4d81 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/NetworkConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/NetworkConfig.java @@ -5,14 +5,32 @@ import lombok.Data; +/** + * Configuration properties for node identity and network settings. + *

+ * Bound from the {@code network.*} prefix in {@code application.yml}. + * These settings identify the current node and configure network-level + * parameters such as multicast discovery and TCP timeouts. + */ @Validated @ConfigurationProperties(prefix = "network") @Data public class NetworkConfig { + /** Unique identifier for this node within the cluster. */ private String nodeId; + + /** Hostname or IP address the node binds to for incoming connections. */ private String bindHost; + + /** Port the node binds to for incoming connections. */ private int bindPort; + + /** Multicast group address used for node discovery. */ private String multicastGroup; + + /** Port used for multicast discovery. */ private int multicastPort; + + /** Timeout (ms) for TCP connections between nodes. */ private int tcpTimeoutMillis; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/RestClientConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/RestClientConfig.java index 00be21b..acdd5c4 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/RestClientConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/RestClientConfig.java @@ -14,6 +14,13 @@ @Configuration public class RestClientConfig { + /** + * Creates a {@link RestClient} with connect and read timeouts derived from + * {@link ClusterConfig#getWriteTimeoutMillis()}, falling back to 5000ms. + * + * @param clusterConfig cluster configuration providing timeout values + * @return a configured {@link RestClient} instance + */ @Bean public RestClient restClient(ClusterConfig clusterConfig) { long timeoutMs = clusterConfig.getWriteTimeoutMillis(); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ScaleCubeConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ScaleCubeConfig.java index ac63710..f28948a 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ScaleCubeConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/ScaleCubeConfig.java @@ -19,6 +19,20 @@ import java.util.ArrayList; import java.util.List; +/** + * Spring configuration that bootstraps a ScaleCube Microservices node for + * cluster membership and service discovery. + *

+ * Each application instance joins a ScaleCube cluster by resolving seed + * members via DNS (headless Kubernetes service) and advertising its pod IP. + * The node exposes a simple {@link PingService} to verify cluster connectivity. + *

+ * Active only when the {@code test} profile is not active or the + * {@code scalecube-single-node} profile is active. + * + * @see edu.yu.capstone.DistributedSecretsVault.service.internal.NodeClient + * @see edu.yu.capstone.DistributedSecretsVault.health.ScaleCubeHealthIndicator + */ @Configuration @Profile("!test | scalecube-single-node") public class ScaleCubeConfig { @@ -27,8 +41,18 @@ public class ScaleCubeConfig { private static final int DEFAULT_DNS_RESOLVE_MAX_ATTEMPTS = 5; private static final long DEFAULT_DNS_RESOLVE_RETRY_DELAY_MS = 1000L; + /** + * Functional interface abstracting DNS resolution for testability. + */ @FunctionalInterface interface DnsResolver { + /** + * Resolve all IP addresses for the given hostname. + * + * @param host the hostname to resolve + * @return array of resolved {@link InetAddress}es + * @throws UnknownHostException if the host cannot be resolved + */ InetAddress[] resolveAllByName(String host) throws UnknownHostException; } @@ -42,6 +66,18 @@ public interface PingService { private Microservices microservices; + /** + * Creates and starts a ScaleCube {@link Microservices} node. + *

+ * The method reads environment variables ({@code POD_IP}, {@code CLUSTER_PORT}, + * {@code SEED_DNS_HOST}, {@code SEED_DNS_PORT}, {@code NODE_NAME}) to configure + * the node's address, cluster port, and seed members. Falls back to safe + * defaults for local (non-Kubernetes) development. + * + * @return a fully started {@link Microservices} instance + * @throws IllegalStateException if {@code SEED_DNS_HOST} is not set or DNS + * resolution fails after all retry attempts + */ @Bean public Microservices scalecubeMicroservices() { // 1. Read environment variables mapped from the k8s manifest @@ -104,6 +140,9 @@ public Microservices scalecubeMicroservices() { return microservices; } + /** + * Gracefully shuts down the ScaleCube node on application context closure. + */ @PreDestroy public void stopScaleCube() { if (microservices != null) { @@ -112,6 +151,17 @@ public void stopScaleCube() { } } + /** + * Attempts DNS resolution with retry logic. + * + * @param seedDnsHost the hostname to resolve (e.g. headless k8s service) + * @param seedDnsPort port each seed member listens on + * @param maxAttempts maximum number of resolution attempts + * @param retryDelayMs delay (ms) between retries + * @param dnsResolver abstraction for {@link InetAddress#getAllByName} + * @return array of resolved ScaleCube {@link Address}es + * @throws IllegalStateException if all attempts fail + */ static Address[] resolveSeedMembersWithRetry(String seedDnsHost, int seedDnsPort, int maxAttempts, long retryDelayMs, DnsResolver dnsResolver) { RuntimeException lastFailure = null; @@ -134,6 +184,16 @@ static Address[] resolveSeedMembersWithRetry(String seedDnsHost, int seedDnsPort lastFailure); } + /** + * Resolves all IP addresses for the given DNS host and converts them to + * ScaleCube {@link Address}es. + * + * @param seedDnsHost the hostname to resolve + * @param seedDnsPort port to pair with each resolved IP + * @param dnsResolver abstraction for DNS lookups + * @return array of ScaleCube addresses + * @throws IllegalStateException if DNS returns no addresses or lookup fails + */ static Address[] resolveSeedMembers(String seedDnsHost, int seedDnsPort, DnsResolver dnsResolver) { try { InetAddress[] addresses = dnsResolver.resolveAllByName(seedDnsHost); @@ -150,6 +210,9 @@ static Address[] resolveSeedMembers(String seedDnsHost, int seedDnsPort, DnsReso } } + /** + * Sleeps for the specified duration, restoring the interrupt flag if interrupted. + */ private static void sleepQuietly(long retryDelayMs) { try { Thread.sleep(retryDelayMs); @@ -159,6 +222,12 @@ private static void sleepQuietly(long retryDelayMs) { } } + /** + * Reads a value from environment variables, falling back to system properties. + * + * @param key the environment variable / system property name + * @return the value, or {@code null} if not set + */ private static String readEnvOrSystemProperty(String key) { String value = System.getenv(key); if (value == null || value.isBlank()) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/SecurityConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/SecurityConfig.java index 06157e6..9e84c5e 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/SecurityConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/SecurityConfig.java @@ -5,11 +5,24 @@ import lombok.Data; +/** + * Configuration properties for authentication and authorization. + *

+ * Bound from the {@code security.*} prefix in {@code application.yml}. + * When {@link #authEnabled} is {@code true}, incoming requests must include + * a valid OAuth token issued by {@link #oauthIssuer} for the configured + * {@link #oauthAudience}. + */ @Validated @ConfigurationProperties(prefix = "security") @Data public class SecurityConfig { + /** Whether authentication is enforced for incoming API requests. */ private boolean authEnabled; + + /** OAuth 2.0 issuer URL used to validate JWT tokens (e.g. {@code https://accounts.google.com}). */ private String oauthIssuer; + + /** Expected OAuth audience claim in the JWT. */ private String oauthAudience; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/StorageConfig.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/StorageConfig.java index be5ff2e..0beb2eb 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/StorageConfig.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/config/StorageConfig.java @@ -5,11 +5,25 @@ import lombok.Data; +/** + * Configuration properties for the Redis storage backend. + *

+ * Bound from the {@code storage.*} prefix in {@code application.yml}. + * Each node connects to its own Redis instance (sidecar pattern) to store + * its assigned secret shards. + * + * @see edu.yu.capstone.DistributedSecretsVault.repository.impl.RedisSecretPartRepository + */ @Validated @ConfigurationProperties(prefix = "storage") @Data public class StorageConfig { + /** Hostname of the Redis instance. */ private String redisHost; + + /** Port of the Redis instance. */ private int redisPort; + + /** Password for Redis authentication (empty string if unauthenticated). */ private String redisPassword; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/ClusterController.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/ClusterController.java index a250f25..c00b00d 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/ClusterController.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/ClusterController.java @@ -14,6 +14,13 @@ import edu.yu.capstone.DistributedSecretsVault.config.ScaleCubeConfig.PingService; import edu.yu.capstone.DistributedSecretsVault.dto.response.ClusterStatusResponse; +/** + * REST controller for cluster health and diagnostics. + *

+ * Provides endpoints to verify cluster connectivity, list discovered nodes, + * and retrieve a summary of cluster health. Only available when ScaleCube + * is active (i.e., when a {@link Microservices} bean exists). + */ @RestController @RequestMapping("/api/v1/cluster") @ConditionalOnBean(Microservices.class) @@ -22,6 +29,14 @@ public class ClusterController { @Autowired private Microservices microservices; + /** + * Sends a ping request through ScaleCube to any available node in the cluster. + *

+ * Useful for verifying that the ScaleCube service mesh is operational and + * that at least one peer can process requests. + * + * @return a "Pong from {nodeName}" response from the receiving node + */ @GetMapping("/ping") public ResponseEntity ping() { // Creates a proxy that routes the ping request over ScaleCube to any available node @@ -30,6 +45,11 @@ public ResponseEntity ping() { return ResponseEntity.ok(response); } + /** + * Lists all nodes discovered by ScaleCube's service discovery. + * + * @return list of strings in the format {@code "nodeId @ host:port"} + */ @GetMapping("/nodes") public ResponseEntity> listNodes() { List nodes = microservices.serviceEndpoints().stream() @@ -38,6 +58,14 @@ public ResponseEntity> listNodes() { return ResponseEntity.ok(nodes); } + /** + * Returns a summary of the cluster's health. + *

+ * Currently uses a simplified model where all discovered nodes are + * assumed healthy. + * + * @return a {@link ClusterStatusResponse} with node counts + */ @GetMapping("/status") public ResponseEntity status() { ClusterStatusResponse response = new ClusterStatusResponse(); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/InternalController.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/InternalController.java index 8dc18ad..a657a60 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/InternalController.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/InternalController.java @@ -26,6 +26,18 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +/** + * Internal REST controller for node-to-node communication. + *

+ * Endpoints under {@code /internal} are called by peer nodes during the + * prepare phase of distributed operations and for fetching individual shards. + * These endpoints are not intended for external client use. + *

+ * Commit messages are delivered via Kafka rather than HTTP, so there are no + * commit endpoints here. + * + * @see edu.yu.capstone.DistributedSecretsVault.service.internal + */ @RestController @RequestMapping("/internal") public class InternalController { @@ -36,6 +48,9 @@ public class InternalController { private final DeletePrepareHandler deletePrepareHandler; private final RepairPrepareHandler repairPrepareHandler; + /** + * Constructs the controller with all prepare handlers and the internal get service. + */ public InternalController(InternalGetService internalGetService, PostPrepareHandler postPrepareHandler, PutPrepareHandler putPrepareHandler, @@ -48,6 +63,14 @@ public InternalController(InternalGetService internalGetService, this.repairPrepareHandler = repairPrepareHandler; } + /** + * Retrieves a single shard from this node's local Redis, optionally at a specific version. + * + * @param id the secret name + * @param user the secret owner + * @param version optional version; if omitted, the latest version is returned + * @return the {@link SecretPart} stored on this node + */ @GetMapping("/{id}") public ResponseEntity getSecretPart(@PathVariable String id, @RequestParam(value = "user") String user, @@ -55,30 +78,71 @@ public ResponseEntity getSecretPart(@PathVariable String id, return internalGetService.getVersion(user, id, version); } + /** + * Retrieves all version shards from this node's local Redis. + * + * @param id the secret name + * @param user the secret owner + * @return map of version numbers to {@link SecretPart}s + */ @GetMapping("/{id}/all") public ResponseEntity> getAllVersions(@PathVariable String id, @RequestParam(value = "user") String user) { return internalGetService.getAllVersions(user, id); } + /** + * Handles a prepare request for a distributed create (POST) operation. + * Buffers the shard locally and returns 204 No Content as an ACK. + * + * @param request the prepare request containing the shard and operation ID + * @return HTTP 204 No Content on success + */ @PostMapping("/prepare") public ResponseEntity preparePost(@RequestBody PostPrepareRequest request) { postPrepareHandler.handle(request); return ResponseEntity.noContent().build(); } + /** + * Handles a prepare request for a distributed update (PUT) operation. + * Buffers the updated shard locally and returns 204 No Content as an ACK. + * + * @param request the prepare request containing the updated shard and operation ID + * @return HTTP 204 No Content on success + */ @PutMapping("/prepare") public ResponseEntity preparePut(@RequestBody PutPrepareRequest request) { putPrepareHandler.handle(request); return ResponseEntity.noContent().build(); } + /** + * Handles a prepare request for a read-repair operation. + * Buffers the repair shard locally and returns 204 No Content as an ACK. + * + * @param request the repair prepare request containing the shard + * @return HTTP 204 No Content on success + */ @PostMapping("/repair/prepare") public ResponseEntity prepareRepair(@RequestBody RepairPrepareRequest request) { repairPrepareHandler.handle(request); return ResponseEntity.noContent().build(); } + /** + * Handles a prepare request for a distributed delete operation. + * Buffers the delete intent locally and returns 204 No Content as an ACK. + *

+ * Unlike other prepare endpoints, delete uses query parameters instead of + * a JSON body because HTTP DELETE with a body is not universally supported. + * + * @param originatorNodeId the node that initiated the delete + * @param operationId UUID correlating this prepare to the eventual commit + * @param secretKeyOwnerId owner of the secret to delete + * @param secretKeyName name of the secret to delete + * @return HTTP 204 No Content on success + */ @DeleteMapping("/prepare") public ResponseEntity prepareDelete( @RequestParam("originatorNodeId") String originatorNodeId, diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/SecretController.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/SecretController.java index 3746d96..8190725 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/SecretController.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/controller/SecretController.java @@ -28,6 +28,17 @@ import java.nio.charset.StandardCharsets; import java.util.Map; +/** + * Public-facing REST controller for secret CRUD operations. + *

+ * All endpoints are scoped under {@code /api/v1/secrets}. Secrets are + * identified by a user-provided name and are owner-scoped (each user + * sees only their own secrets). The controller delegates to dedicated + * service classes that orchestrate the distributed two-phase commit + * protocol across the cluster. + * + * @see edu.yu.capstone.DistributedSecretsVault.service.secret + */ @RestController @RequestMapping("/api/v1/secrets") public class SecretController { @@ -38,6 +49,9 @@ public class SecretController { private final DeleteSecretService deleteSecretService; private final EnvFileService envFileService; + /** + * Constructs the controller with all required secret service dependencies. + */ public SecretController(GetSecretService getSecretService, PostSecretService postSecretService, PutSecretService putSecretService, DeleteSecretService deleteSecretService, EnvFileService envFileService) { @@ -48,27 +62,65 @@ public SecretController(GetSecretService getSecretService, PostSecretService pos this.envFileService = envFileService; } + /** + * Retrieves a single secret value, optionally at a specific version. + * + * @param id the secret name + * @param user the owner of the secret + * @param version optional version number; if omitted, the latest version is returned + * @return the reconstructed plaintext secret value + */ @GetMapping("/{id}") public ResponseEntity getSecret(@PathVariable String id, @RequestParam("user") String user, @RequestParam(value = "version", required = false) Long version) { return getSecretService.getVersion(user, id, version); } + /** + * Retrieves all versions of a secret as a map of version number to plaintext value. + * + * @param id the secret name + * @param user the owner of the secret + * @return map of version numbers to reconstructed secret values + */ @GetMapping("/{id}/all") public ResponseEntity> getAllSecrets(@PathVariable String id, @RequestParam("user") String user) { return getSecretService.getAllVersions(user, id); } + /** + * Creates a new secret. The secret value is split into Shamir shards and + * distributed across the cluster using the two-phase commit protocol. + * + * @param request the create request containing name, value, and owner + * @return HTTP 201 with a confirmation message including the assigned version + */ @PostMapping public ResponseEntity postSecret(@RequestBody PostSecretRequest request) { return postSecretService.execute(request); } + /** + * Imports secrets from a {@code .env} file sent as plain text. + * Each {@code KEY=VALUE} line becomes a separate secret. + * + * @param user the owner for the imported secrets + * @param envFileContent raw text content of the {@code .env} file + * @return confirmation message summarizing the import + */ @PostMapping(value = "/env", consumes = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity postEnvFile(@RequestParam("user") String user, @RequestBody String envFileContent) { return envFileService.execute(user, envFileContent); } + /** + * Imports secrets from a {@code .env} file uploaded as a multipart form. + * + * @param user the owner for the imported secrets + * @param file the uploaded {@code .env} file + * @return confirmation message summarizing the import + * @throws IllegalArgumentException if the file cannot be read + */ @PostMapping(value = "/env", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity postEnvFile(@RequestParam("user") String user, @RequestParam("file") MultipartFile file) { @@ -79,6 +131,12 @@ public ResponseEntity postEnvFile(@RequestParam("user") String user, } } + /** + * Imports secrets from a {@code .env} file sent as a JSON request body. + * + * @param request JSON body containing the owner and env file content + * @return confirmation message summarizing the import + */ @PostMapping(value = "/env", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity postEnvFile(@RequestBody EnvFileRequest request) { if (request == null) { @@ -87,11 +145,23 @@ public ResponseEntity postEnvFile(@RequestBody EnvFileRequest request) { return envFileService.execute(request.getUser(), request.getEnvFileContent()); } + /** + * Updates an existing secret with a new value, creating a new version. + * + * @param request the update request containing the secret name, new value, and owner + * @return confirmation message including the new version number + */ @PutMapping public ResponseEntity updateSecret(@RequestBody PutSecretRequest request) { return putSecretService.execute(request); } + /** + * Deletes a secret and all of its versions from every node in the cluster. + * + * @param request the delete request containing the secret name and owner + * @return HTTP 204 No Content on success + */ @DeleteMapping public ResponseEntity deleteSecret(@RequestBody DeleteSecretRequest request) { return deleteSecretService.execute(request); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretKey.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretKey.java index eefe4cd..9f1f6b1 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretKey.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretKey.java @@ -4,10 +4,21 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Composite key that uniquely identifies a secret within the vault. + *

+ * A secret is scoped to an {@link #ownerId owner} (the user who created it) + * and a human-readable {@link #name}. Together these form the Redis key + * used by {@link edu.yu.capstone.DistributedSecretsVault.repository.impl.RedisSecretPartRepository} + * in the format {@code ownerId:name}. + */ @Data @NoArgsConstructor @AllArgsConstructor public class SecretKey { + /** Identifier of the user who owns this secret (e.g. a username or OAuth subject). */ private String ownerId; + + /** Human-readable name chosen by the owner (e.g. {@code "DB_PASSWORD"}). */ private String name; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretPart.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretPart.java index 3f012e9..658c318 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretPart.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretPart.java @@ -4,12 +4,30 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * A single shard (share) of a secret, stored on one node in the cluster. + *

+ * When a secret is created or updated, it is split into {@code N} shards via + * Shamir's Secret Sharing. Each shard is assigned a unique {@link #partIndex} + * (1-based) and distributed to a different node. At least {@code K} shards + * (the threshold) are required to reconstruct the original secret value. + * + * @see edu.yu.capstone.DistributedSecretsVault.service.secret.SecretSharingService + * @see edu.yu.capstone.DistributedSecretsVault.service.secret.SecretReconstructionService + */ @Data @NoArgsConstructor @AllArgsConstructor public class SecretPart { + /** Composite key identifying the secret this shard belongs to. */ private SecretKey key; + + /** Version number of the secret (monotonically increasing per secret). */ private Long version; + + /** 1-based index identifying which share this is in the Shamir split. */ private int partIndex; + + /** Raw byte payload of the Shamir share. */ private byte[] shard; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretVersion.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretVersion.java index 6ff42f3..c31cce1 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretVersion.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/domain/model/SecretVersion.java @@ -3,10 +3,22 @@ import lombok.AllArgsConstructor; import lombok.Data; +/** + * Immutable snapshot representing a successfully committed secret version. + *

+ * Returned by write operations (create, update) as a receipt confirming + * which version was written and the timestamp at which the write was + * initiated. + */ @Data @AllArgsConstructor public class SecretVersion { + /** Composite key identifying the secret. */ private SecretKey key; + + /** Monotonically increasing version number assigned at write time. */ private long version; + + /** Wall-clock timestamp (millis since epoch) when the write was initiated. */ private long timestampMillis; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/CommitMessage.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/CommitMessage.java index 2fd6175..13f6ec4 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/CommitMessage.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/CommitMessage.java @@ -8,11 +8,24 @@ import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +/** + * Kafka message payload broadcast during the commit phase of a distributed operation. + *

+ * After the originator node collects enough prepare ACKs, it publishes a + * {@code CommitMessage} to the {@link edu.yu.capstone.DistributedSecretsVault.config.KafkaConfig#COMMIT_TOPIC} + * topic. Every node (including the originator) consumes the message and + * finalizes the buffered action via {@link edu.yu.capstone.DistributedSecretsVault.service.communication.CommitDispatcher}. + */ @Data @NoArgsConstructor @AllArgsConstructor public class CommitMessage { + /** Unique identifier correlating this commit to its prepare phase. */ private UUID operationId; + + /** The secret key targeted by this operation. */ private SecretKey secretKey; + + /** The type of distributed operation (POST, PUT, DELETE, REPAIR). */ private ActionType actionType; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostCommitRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostCommitRequest.java index 3111527..ad5708e 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostCommitRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostCommitRequest.java @@ -7,10 +7,20 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body dispatched to each node during the commit phase + * of a distributed create (POST). + *

+ * Upon receiving this, each node persists the shard it buffered during + * the prepare phase and removes the pending entry. + */ @Data @NoArgsConstructor @AllArgsConstructor public class PostCommitRequest { + /** UUID matching the corresponding {@link PostPrepareRequest#getOperationId()}. */ private UUID operationId; + + /** The secret key identifying which secret to create. */ private SecretKey secretKey; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostPrepareRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostPrepareRequest.java index 8385ccb..ee80637 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostPrepareRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PostPrepareRequest.java @@ -6,11 +6,24 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body sent by the originator node to all peers during the + * prepare phase of a distributed create (POST). + *

+ * Each peer buffers the shard in {@link edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer} + * and responds with an ACK. The originator collects ACKs and, upon reaching + * quorum, broadcasts a commit via Kafka. + */ @Data @NoArgsConstructor @AllArgsConstructor public class PostPrepareRequest { + /** Node ID of the originator that initiated the create. */ private String originatorNodeId; + + /** UUID correlating prepare → commit for this create operation. */ private UUID operationId; + + /** The shard payload to be buffered on the receiving peer. */ private SecretPartMessage secretPartMessage; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutCommitRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutCommitRequest.java index b42e1af..fe7ef3d 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutCommitRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutCommitRequest.java @@ -7,10 +7,20 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body dispatched to each node during the commit phase + * of a distributed update (PUT). + *

+ * Upon receiving this, each node applies the updated shard it buffered during + * the prepare phase and removes the pending entry. + */ @Data @NoArgsConstructor @AllArgsConstructor public class PutCommitRequest { + /** UUID matching the corresponding {@link PutPrepareRequest#getOperationId()}. */ private UUID operationId; + + /** The secret key identifying which secret to update. */ private SecretKey secretKey; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutPrepareRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutPrepareRequest.java index b7499ee..f0c2aca 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutPrepareRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/PutPrepareRequest.java @@ -6,11 +6,24 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body sent by the originator node to all peers during the + * prepare phase of a distributed update (PUT). + *

+ * Each peer verifies the secret exists, buffers the updated shard in + * {@link edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer}, + * and responds with an ACK. + */ @Data @NoArgsConstructor @AllArgsConstructor public class PutPrepareRequest { + /** Node ID of the originator that initiated the update. */ private String originatorNodeId; + + /** UUID correlating prepare → commit for this update operation. */ private UUID operationId; + + /** The updated shard payload to be buffered on the receiving peer. */ private SecretPartMessage secretPartMessage; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairCommitRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairCommitRequest.java index 23697d5..97af933 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairCommitRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairCommitRequest.java @@ -7,10 +7,20 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body dispatched to each node during the commit phase + * of a read-repair operation. + *

+ * Upon receiving this, each node persists the repair shard it buffered during + * the prepare phase and removes the pending entry. + */ @Data @NoArgsConstructor @AllArgsConstructor public class RepairCommitRequest { + /** UUID matching the corresponding {@link RepairPrepareRequest#getOperationId()}. */ private UUID operationId; + + /** The secret key identifying which secret is being repaired. */ private SecretKey secretKey; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairPrepareRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairPrepareRequest.java index 5751aad..140dcaf 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairPrepareRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/RepairPrepareRequest.java @@ -6,11 +6,23 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Request body sent by the originator node to peers during the + * prepare phase of a read-repair operation. + *

+ * Read-repair re-distributes shards to nodes that are missing them, + * bringing the cluster back to full redundancy. + */ @Data @NoArgsConstructor @AllArgsConstructor public class RepairPrepareRequest { + /** Node ID of the originator that initiated the repair. */ private String originatorNodeId; + + /** UUID correlating prepare → commit for this repair operation. */ private UUID operationId; + + /** The shard payload to be buffered on the receiving peer. */ private SecretPartMessage secretPartMessage; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/SecretPartMessage.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/SecretPartMessage.java index 5f43a7f..fd28c5a 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/SecretPartMessage.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/internal/SecretPartMessage.java @@ -5,13 +5,30 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Wire-format DTO carrying a single secret shard between nodes during the + * prepare phase of a write operation (POST, PUT, or REPAIR). + *

+ * This is the serialized form that travels over HTTP; on the receiving side + * it is converted to a {@link edu.yu.capstone.DistributedSecretsVault.domain.model.SecretPart} + * and buffered in {@link edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer}. + */ @Data @NoArgsConstructor @AllArgsConstructor public class SecretPartMessage { + /** Composite key identifying which secret this shard belongs to. */ private SecretKey key; + + /** Version number of the secret being written. */ private Long version; + + /** Raw Shamir share bytes. */ private byte[] shard; + + /** Wall-clock timestamp (ms since epoch) when the write was initiated. */ private long timestampMillis; + + /** 1-based index of this share in the Shamir split. */ private int partIndex; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/ClusterStatusResponse.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/ClusterStatusResponse.java index aab636b..597b864 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/ClusterStatusResponse.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/ClusterStatusResponse.java @@ -2,10 +2,21 @@ import lombok.Data; +/** + * API response body for the {@code /api/v1/cluster/status} endpoint, + * summarizing the health of the cluster. + */ @Data public class ClusterStatusResponse { + /** Total number of known nodes in the cluster. */ private int totalNodes; + + /** Number of nodes currently considered healthy. */ private int healthyNodes; + + /** Number of nodes in a suspect (unreachable) state. */ private int suspectNodes; + + /** Number of nodes confirmed as failed. */ private int failedNodes; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretResponse.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretResponse.java index 13b31b9..b20eca7 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretResponse.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretResponse.java @@ -2,9 +2,17 @@ import lombok.Data; +/** + * API response body containing a single secret value and its metadata. + */ @Data public class SecretResponse { + /** The secret's name (key). */ private String key; + + /** The version number of this secret value. */ private long version; + + /** The reconstructed plaintext secret value. */ private String value; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretVersionResponse.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretVersionResponse.java index 3cf2f41..106f579 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretVersionResponse.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/response/SecretVersionResponse.java @@ -2,8 +2,14 @@ import lombok.Data; +/** + * API response body for a single version of a secret. + */ @Data public class SecretVersionResponse { + /** The version number. */ private long version; + + /** The reconstructed plaintext value for this version. */ private String value; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/DeleteSecretRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/DeleteSecretRequest.java index ccde9d7..8309ed6 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/DeleteSecretRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/DeleteSecretRequest.java @@ -4,10 +4,16 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * API request body for deleting a secret ({@code DELETE /api/v1/secrets}). + */ @Data @NoArgsConstructor @AllArgsConstructor public class DeleteSecretRequest { + /** Name (key) of the secret to delete. */ private String deleteName; + + /** Owner (user) requesting the deletion. */ private String user; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/EnvFileRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/EnvFileRequest.java index 96a3c88..8e4efdf 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/EnvFileRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/EnvFileRequest.java @@ -6,12 +6,21 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * API request body for importing secrets from a {@code .env} file + * ({@code POST /api/v1/secrets/env} with {@code application/json}). + *

+ * The {@link #envFileContent} field accepts multiple JSON aliases + * ({@code content}, {@code env}, {@code envFile}) for client convenience. + */ @Data @NoArgsConstructor @AllArgsConstructor public class EnvFileRequest { + /** Owner (user) importing the env file. */ private String user; + /** Raw text content of the {@code .env} file (e.g. {@code KEY=VALUE} per line). */ @JsonAlias({ "content", "env", "envFile" }) private String envFileContent; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PostSecretRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PostSecretRequest.java index 1adf9fc..dafff3a 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PostSecretRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PostSecretRequest.java @@ -4,11 +4,19 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * API request body for creating a new secret ({@code POST /api/v1/secrets}). + */ @Data @NoArgsConstructor @AllArgsConstructor public class PostSecretRequest { + /** Name (key) for the new secret. */ private String secretName; + + /** Plaintext value of the secret. */ private String secretValue; + + /** Owner (user) creating the secret. */ private String user; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PutSecretRequest.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PutSecretRequest.java index 0b3ef11..2259233 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PutSecretRequest.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/dto/secret/PutSecretRequest.java @@ -4,11 +4,19 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * API request body for updating an existing secret ({@code PUT /api/v1/secrets}). + */ @Data @NoArgsConstructor @AllArgsConstructor public class PutSecretRequest { + /** Current name (key) of the secret to update. */ private String secretCurrentName; + + /** New plaintext value to replace the existing secret. */ private String secretUpdatedValue; + + /** Owner (user) requesting the update. */ private String user; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretReconstructor.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretReconstructor.java index 53d493b..1f140fd 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretReconstructor.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretReconstructor.java @@ -2,7 +2,19 @@ import java.util.Map; +/** + * Convenience wrapper that delegates secret reconstruction to {@link ShamirSecretSharing}. + *

+ * Used by {@link edu.yu.capstone.DistributedSecretsVault.service.secret.SecretReconstructionService} + * to reassemble a plaintext secret from collected Shamir shares. + */ public class SecretReconstructor { + /** + * Reconstructs a secret from Shamir shares. + * + * @param parts map of share index (1-based) to share bytes + * @return the reconstructed secret bytes + */ public byte[] reconstruct(Map parts) { return new ShamirSecretSharing().reconstruct(parts); } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretSplitter.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretSplitter.java index 87e0b2d..3b5c561 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretSplitter.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/SecretSplitter.java @@ -2,7 +2,21 @@ import java.util.Map; +/** + * Convenience wrapper that delegates secret splitting to {@link ShamirSecretSharing}. + *

+ * Used by {@link edu.yu.capstone.DistributedSecretsVault.service.secret.SecretSharingService} + * to convert a plaintext secret value into Shamir shares. + */ public class SecretSplitter { + /** + * Splits a secret into Shamir shares. + * + * @param secret raw secret bytes + * @param totalParts total number of shares to generate + * @param threshold minimum shares required to reconstruct + * @return map of share index (1-based) to share bytes + */ public Map split(byte[] secret, int totalParts, int threshold) { return new ShamirSecretSharing().split(secret, totalParts, threshold); } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/ShamirSecretSharing.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/ShamirSecretSharing.java index daed50a..b4e5f8b 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/ShamirSecretSharing.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/encrypt/ShamirSecretSharing.java @@ -6,7 +6,28 @@ import com.codahale.shamir.Scheme; +/** + * Low-level implementation of Shamir's Secret Sharing scheme. + *

+ * Wraps the {@link com.codahale.shamir.Scheme} library to split a byte-array + * secret into {@code N} shares such that any {@code K} (threshold) shares can + * reconstruct the original. When {@code threshold == 1}, all shares are simply + * copies of the secret (degenerate case). + * + * @see SecretSplitter + * @see SecretReconstructor + */ public class ShamirSecretSharing { + /** + * Splits a secret into multiple shares using Shamir's scheme. + * + * @param secret the raw secret bytes to split + * @param totalParts total number of shares to generate (N) + * @param threshold minimum shares required for reconstruction (K) + * @return map of share index (1-based) to share bytes + * @throws IllegalArgumentException if inputs are null, non-positive, or + * if {@code threshold > totalParts} + */ public Map split(byte[] secret, int totalParts, int threshold) { if (secret == null) { throw new IllegalArgumentException("Secret bytes are required"); @@ -28,6 +49,17 @@ public Map split(byte[] secret, int totalParts, int threshold) return scheme.split(secret); } + /** + * Reconstructs a secret from a set of Shamir shares. + *

+ * The number of shares provided determines the threshold used for + * reconstruction. When only one share is present, it is returned directly + * (degenerate case). + * + * @param parts map of share index (1-based) to share bytes + * @return the reconstructed secret bytes + * @throws IllegalArgumentException if {@code parts} is null or empty + */ public byte[] reconstruct(Map parts) { if (parts == null || parts.isEmpty()) { throw new IllegalArgumentException("Secret parts are required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/GlobalExceptionHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/GlobalExceptionHandler.java index 879d8d5..b931c46 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/GlobalExceptionHandler.java @@ -6,6 +6,16 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +/** + * Centralized exception-to-HTTP-status mapping for the entire application. + *

+ * Each handler method catches a specific domain exception, selects the + * appropriate HTTP status code, and returns a uniform {@link ErrorResponse} + * body. This ensures clients always receive a consistent error format + * regardless of which controller triggered the exception. + *

+ * Status code assignments follow the design documents in {@code docs/crud/}. + */ @ControllerAdvice public class GlobalExceptionHandler { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/NodeCommunicationException.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/NodeCommunicationException.java index 5050e06..dc8270a 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/NodeCommunicationException.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/NodeCommunicationException.java @@ -1,5 +1,13 @@ package edu.yu.capstone.DistributedSecretsVault.exceptions; +/** + * Thrown when an HTTP request to a peer node fails due to a network error, + * timeout, or unexpected response. + * + * Maps to HTTP 503 Service Unavailable. + * + * @see docs/crud/retrieve.md §9 – Node Unavailable + */ public class NodeCommunicationException extends ServiceUnavailableException { public NodeCommunicationException() { super("Node communication failure"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/VersionConflictException.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/VersionConflictException.java index c6dce61..0fd84c5 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/VersionConflictException.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/exceptions/VersionConflictException.java @@ -1,5 +1,12 @@ package edu.yu.capstone.DistributedSecretsVault.exceptions; +/** + * Thrown when a write operation discovers that the version being written + * has already been superseded (e.g. a concurrent update incremented the + * version number before the current write could commit). + * + * Maps to HTTP 409 Conflict. + */ public class VersionConflictException extends RuntimeException { public VersionConflictException() { super("Version conflict detected"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/health/ScaleCubeHealthIndicator.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/health/ScaleCubeHealthIndicator.java index 13ffac0..549c41b 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/health/ScaleCubeHealthIndicator.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/health/ScaleCubeHealthIndicator.java @@ -8,6 +8,13 @@ import io.scalecube.services.Microservices; +/** + * Spring Boot Actuator health indicator for ScaleCube cluster membership. + *

+ * Reports {@code UP} with the number of discovered service endpoints when + * ScaleCube is initialized, or {@code DOWN} otherwise. Only active when + * a {@link Microservices} bean exists in the context. + */ @Component @ConditionalOnBean(Microservices.class) public class ScaleCubeHealthIndicator implements HealthIndicator { @@ -15,6 +22,12 @@ public class ScaleCubeHealthIndicator implements HealthIndicator { @Autowired private Microservices microservices; + /** + * Checks ScaleCube connectivity and returns health details. + * + * @return {@link Health#up()} with endpoint count, or {@link Health#down()} if + * ScaleCube is not initialized + */ @Override public Health health() { if (microservices == null || microservices.serviceEndpoints() == null) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/SecretPartRepository.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/SecretPartRepository.java index 1f7acc1..24ea2cb 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/SecretPartRepository.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/SecretPartRepository.java @@ -6,18 +6,73 @@ import edu.yu.capstone.DistributedSecretsVault.domain.model.SecretKey; import edu.yu.capstone.DistributedSecretsVault.domain.model.SecretPart; +/** + * Persistence abstraction for storing and retrieving individual secret shards. + *

+ * Each node in the cluster stores at most one shard per secret version. + * Implementations are responsible for versioned storage, existence checks, + * and bulk deletion of all versions for a given key. + * + * @see edu.yu.capstone.DistributedSecretsVault.repository.impl.RedisSecretPartRepository + */ public interface SecretPartRepository { + + /** + * Finds a specific version of a secret shard. + * + * @param key the composite secret key + * @param version the version number to retrieve + * @return the shard, or empty if not found + */ Optional findPart(SecretKey key, long version); + /** + * Finds the latest (highest version) shard for a secret. + * + * @param key the composite secret key + * @return the latest shard, or empty if no versions exist + */ Optional findLatest(SecretKey key); + /** + * Lists all stored version numbers for a secret, in ascending order. + * + * @param key the composite secret key + * @return list of version numbers, or an empty list if none exist + */ List listVersions(SecretKey key); + /** + * Checks whether at least one version exists for the given key. + * + * @param key the composite secret key + * @return {@code true} if the secret has at least one stored version + */ boolean exists(SecretKey key); + /** + * Persists a new secret shard. Used during the commit phase of a create operation. + * + * @param part the shard to save (must include key, version, index, and data) + * @throws IllegalArgumentException if {@code part} or its key is null + */ void savePart(SecretPart part); + /** + * Updates an existing secret shard in place. Used during the commit phase of + * an update operation. + * + * @param part the shard with updated data + * @return {@code true} if the version existed and was updated, {@code false} otherwise + * @throws IllegalArgumentException if {@code part} or its key is null + */ boolean updatePart(SecretPart part); + /** + * Deletes all versions of a secret shard. Used during the commit phase of + * a delete operation. + * + * @param key the composite secret key + */ void deleteParts(SecretKey key); } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/impl/RedisSecretPartRepository.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/impl/RedisSecretPartRepository.java index 7f79d5a..2fa06db 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/impl/RedisSecretPartRepository.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/repository/impl/RedisSecretPartRepository.java @@ -19,6 +19,17 @@ // TODO attempt to switch from using a raw list for listVersionsScript +/** + * Redis-backed implementation of {@link SecretPartRepository}. + *

+ * All persistence operations are executed as atomic Lua scripts to guarantee + * consistency within a single Redis instance. Secret shards are serialized as + * JSON via {@link Gson} and stored in a Redis sorted set keyed by + * {@code ownerId:name}, with the version number as the score. + *

+ * Each Lua script is loaded from the classpath ({@code redis/*.lua}) at + * construction time and cached as a {@link RedisScript}. + */ @Repository public class RedisSecretPartRepository implements SecretPartRepository { private static final String SAVE_PART_PATH = "redis/save_part.lua"; @@ -39,6 +50,11 @@ public class RedisSecretPartRepository implements SecretPartRepository { private final RedisScript updatePartScript; private final RedisScript deleteScript; + /** + * Constructs the repository and pre-loads all Lua scripts from the classpath. + * + * @param redisTemplate Spring Redis template for executing scripts + */ public RedisSecretPartRepository(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.gson = new Gson(); @@ -51,6 +67,7 @@ public RedisSecretPartRepository(StringRedisTemplate redisTemplate) { this.deleteScript = loadScript(DELETE_PATH, Long.class); } + /** {@inheritDoc} */ @Override public Optional findPart(SecretKey key, long version) { String value = redisTemplate.execute(getByVersionScript, List.of(secretKey(key)), String.valueOf(version)); @@ -60,6 +77,7 @@ public Optional findPart(SecretKey key, long version) { return Optional.of(deserialize(value)); } + /** {@inheritDoc} */ @Override public Optional findLatest(SecretKey key) { String value = redisTemplate.execute(getLatestScript, List.of(secretKey(key))); @@ -69,12 +87,14 @@ public Optional findLatest(SecretKey key) { return Optional.of(deserialize(value)); } + /** {@inheritDoc} */ @Override public List listVersions(SecretKey key) { List rawVersions = redisTemplate.execute(listVersionsScript, List.of(secretKey(key))); if (rawVersions == null || rawVersions.isEmpty()) { return List.of(); } + // Lua returns version numbers as strings; convert each to Long List results = new ArrayList<>(rawVersions.size()); for (Object version : rawVersions) { try { @@ -86,12 +106,14 @@ public List listVersions(SecretKey key) { return results; } + /** {@inheritDoc} */ @Override public boolean exists(SecretKey key) { Long size = redisTemplate.execute(existsScript, List.of(secretKey(key))); return size != null && size > 0; } + /** {@inheritDoc} */ @Override public void savePart(SecretPart part) { if (part == null || part.getKey() == null) { @@ -101,6 +123,7 @@ public void savePart(SecretPart part) { String.valueOf(part.getVersion()), serialize(part)); } + /** {@inheritDoc} */ @Override public boolean updatePart(SecretPart part) { if (part == null || part.getKey() == null) { @@ -111,11 +134,20 @@ public boolean updatePart(SecretPart part) { return updated != null && updated > 0; } + /** {@inheritDoc} */ @Override public void deleteParts(SecretKey key) { redisTemplate.execute(deleteScript, List.of(secretKey(key))); } + /** + * Loads a Lua script from the classpath and caches it for repeated execution. + * + * @param path classpath-relative path to the {@code .lua} file + * @param resultType expected return type of the script + * @param result type parameter + * @return a cached {@link RedisScript} + */ private RedisScript loadScript(String path, Class resultType) { DefaultRedisScript script = new DefaultRedisScript<>(); script.setLocation(new ClassPathResource(path)); @@ -123,6 +155,9 @@ private RedisScript loadScript(String path, Class resultType) { return script; } + /** + * Converts a {@link SecretKey} into the Redis key string {@code ownerId:name}. + */ private String secretKey(SecretKey key) { if (key == null || key.getOwnerId() == null || key.getName() == null) { throw new IllegalArgumentException("Secret key is required"); @@ -130,10 +165,12 @@ private String secretKey(SecretKey key) { return key.getOwnerId() + ":" + key.getName(); } + /** Serializes a {@link SecretPart} to JSON. */ private String serialize(SecretPart part) { return gson.toJson(part); } + /** Deserializes JSON back to a {@link SecretPart}. */ private SecretPart deserialize(String value) { try { return gson.fromJson(value, SecretPart.class); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitDispatcher.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitDispatcher.java index aff60c1..93b1507 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitDispatcher.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitDispatcher.java @@ -16,6 +16,19 @@ import edu.yu.capstone.DistributedSecretsVault.service.internal.PutCommitHandler; import edu.yu.capstone.DistributedSecretsVault.service.internal.RepairCommitHandler; +/** + * Routes incoming {@link CommitMessage}s from Kafka to the appropriate + * commit handler based on the {@link ActionType}. + *

+ * Each commit handler finalizes the buffered prepare action for one + * operation type (POST, PUT, DELETE, REPAIR). If no matching buffered + * action exists (e.g. it was already evicted or committed by a different + * message), the resulting {@link InternalOperationConflictException} is + * caught and logged rather than propagated. + * + * @see CommitListener + * @see CommitPublisher + */ @Service public class CommitDispatcher { private static final Logger log = LoggerFactory.getLogger(CommitDispatcher.class); @@ -25,6 +38,9 @@ public class CommitDispatcher { private final PutCommitHandler putCommitHandler; private final RepairCommitHandler repairCommitHandler; + /** + * Constructs the dispatcher with all commit handler implementations. + */ public CommitDispatcher(DeleteCommitHandler deleteCommitHandler, PostCommitHandler postCommitHandler, PutCommitHandler putCommitHandler, @@ -35,6 +51,15 @@ public CommitDispatcher(DeleteCommitHandler deleteCommitHandler, this.repairCommitHandler = repairCommitHandler; } + /** + * Dispatches a commit message to the correct handler based on action type. + *

+ * Stale or conflicting commits (where the buffered prepare was already + * evicted or superseded) are silently ignored. + * + * @param message the Kafka commit message + * @throws IllegalArgumentException if the message or any required field is null + */ public void dispatch(CommitMessage message) { validate(message); try { @@ -56,6 +81,9 @@ public void dispatch(CommitMessage message) { } } + /** + * Validates that all required fields of a commit message are present. + */ private void validate(CommitMessage message) { if (message == null) { throw new IllegalArgumentException("Commit message is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitListener.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitListener.java index 666c62b..dcc85f1 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitListener.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitListener.java @@ -5,14 +5,32 @@ import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; +/** + * Kafka consumer that listens for commit messages on the + * {@link KafkaConfig#COMMIT_TOPIC} topic and delegates them to the + * {@link CommitDispatcher} for processing. + *

+ * Every node in the cluster runs this listener, so a single Kafka commit + * message is consumed by all nodes simultaneously, ensuring every node + * commits the same buffered prepare action. + */ @Service public class CommitListener { private final CommitDispatcher commitDispatcher; + /** + * Constructs the listener with the dispatcher that routes commit messages. + */ public CommitListener(CommitDispatcher commitDispatcher) { this.commitDispatcher = commitDispatcher; } + /** + * Invoked by Spring Kafka when a new {@link CommitMessage} arrives on + * the commit topic. Delegates to the dispatcher for action-specific handling. + * + * @param message the deserialized commit message + */ @KafkaListener(topics = KafkaConfig.COMMIT_TOPIC) public void onCommitMessage(CommitMessage message) { commitDispatcher.dispatch(message); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitPublisher.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitPublisher.java index bcd7b2a..ed5ef76 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitPublisher.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/communication/CommitPublisher.java @@ -8,16 +8,45 @@ import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; +/** + * Publishes {@link CommitMessage}s to the Kafka commit topic. + *

+ * After the originator node collects enough prepare ACKs from peers, + * it calls {@link #broadcastCommit(CommitMessage)} to notify all nodes + * (including itself) to finalize the buffered action. The send is + * synchronous with a configurable timeout to ensure delivery is confirmed + * before the caller returns. + * + * @see CommitListener + * @see CommitDispatcher + */ @Service public class CommitPublisher { + /** Maximum time to wait for Kafka send confirmation. */ private static final long PUBLISH_TIMEOUT_SECONDS = 10; private final KafkaTemplate kafkaTemplate; + /** + * Constructs the publisher with a Spring Kafka template. + * + * @param kafkaTemplate template for sending Kafka messages + */ public CommitPublisher(KafkaTemplate kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } + /** + * Synchronously publishes a commit message to the Kafka commit topic. + *

+ * The message is keyed by the operation ID to ensure ordering for the + * same operation. Blocks up to {@link #PUBLISH_TIMEOUT_SECONDS} seconds + * for broker acknowledgment. + * + * @param message the commit message to broadcast + * @throws IllegalArgumentException if the message or operation ID is null + * @throws ServiceUnavailableException if publishing fails or is interrupted + */ public void broadcastCommit(CommitMessage message) { if (message == null || message.getOperationId() == null) { throw new IllegalArgumentException("Commit message and operation ID are required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalGetService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalGetService.java index 5a77107..20961aa 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalGetService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalGetService.java @@ -22,6 +22,18 @@ import java.util.Optional; import java.util.TreeSet; +/** + * Orchestrates secret retrieval across the cluster. + *

+ * For cluster-wide reads, this service collects shards from the local + * repository and all reachable peer nodes, then uses + * {@link SecretReconstructionService} to reconstruct the plaintext secret + * via Shamir's scheme. If the number of available shards is near the + * threshold, an automatic read-repair is triggered. + *

+ * For internal (node-local) reads, it serves individual shards directly + * from the local Redis. + */ @Service public class InternalGetService { private static final Logger log = LoggerFactory.getLogger(InternalGetService.class); @@ -44,6 +56,16 @@ public InternalGetService(SecretPartRepository secretPartRepository, this.internalRepairService = internalRepairService; } + /** + * Retrieves a secret across the cluster by collecting shards from all nodes. + * Optionally triggers read-repair if shard count is near the threshold. + * + * @param key the composite secret key + * @param version optional version; if null, fetches the latest + * @return the reconstructed plaintext secret value + * @throws SecretNotFoundException if no shards are found + * @throws InsufficientShardsException if not enough shards for reconstruction + */ public String getAcrossCluster(SecretKey key, Long version) { validateKey(key); ReconstructionParts reconstructionParts = collectPartsForReconstruction(key, version); @@ -52,6 +74,13 @@ public String getAcrossCluster(SecretKey key, Long version) { return value; } + /** + * Retrieves all versions of a secret across the cluster, reconstructing each. + * + * @param key the composite secret key + * @return map of version numbers to reconstructed plaintext values + * @throws SecretNotFoundException if no versions are found + */ public Map getAllVersionsAcrossCluster(SecretKey key) { validateKey(key); Map> partsByVersion = collectAllPartsByVersion(key); @@ -66,6 +95,15 @@ public Map getAllVersionsAcrossCluster(SecretKey key) { return results; } + /** + * Retrieves a single shard from the local node's Redis (called by peers). + * + * @param user the secret owner + * @param secretName the secret name + * @param version optional version; if null, returns the latest + * @return the local {@link SecretPart} + * @throws SecretNotFoundException if the secret or version is not found locally + */ public ResponseEntity getVersion(String user, String secretName, Long version) { SecretKey key = validate(user, secretName); if (!secretPartRepository.exists(key)) { @@ -80,6 +118,14 @@ public ResponseEntity getVersion(String user, String secretName, Lon return ResponseEntity.ok(part.get()); } + /** + * Retrieves all version shards from the local node's Redis (called by peers). + * + * @param user the secret owner + * @param secretName the secret name + * @return map of version numbers to local {@link SecretPart}s + * @throws SecretNotFoundException if no versions are found locally + */ public ResponseEntity> getAllVersions(String user, String secretName) { SecretKey key = validate(user, secretName); List versions = secretPartRepository.listVersions(key); @@ -259,6 +305,10 @@ private boolean hasPartForVersion(Optional part, long version) { && part.get().getVersion() == version; } + /** + * Intermediate result holding the shards selected for reconstruction + * and metadata needed for read-repair decisions. + */ private record ReconstructionParts(List selectedParts, long version, int availableParts, int liveRepairTargets) { } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPostService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPostService.java index e5ee909..2a1d4eb 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPostService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPostService.java @@ -22,6 +22,20 @@ import edu.yu.capstone.DistributedSecretsVault.service.internal.NodeClient.PeerResponse; import edu.yu.capstone.DistributedSecretsVault.service.secret.SecretSharingService; +/** + * Orchestrates the distributed two-phase commit for creating a new secret. + *

+ * Flow: + *

    + *
  1. Split the plaintext into Shamir shards
  2. + *
  3. Buffer the local shard and broadcast prepare requests to peers
  4. + *
  5. If quorum is reached, publish a commit message via Kafka
  6. + *
  7. If quorum fails, discard the buffered action
  8. + *
+ * + * @see PostPrepareHandler + * @see PostCommitHandler + */ @Service public class InternalPostService { private static final Logger log = LoggerFactory.getLogger(InternalPostService.class); @@ -55,6 +69,16 @@ public InternalPostService(NodeClient nodeClient, ? envNodeId : "local-node"; } + /** + * Creates a secret across the cluster using the two-phase commit protocol. + * + * @param key composite key identifying the secret + * @param value plaintext secret value to store + * @return the {@link SecretVersion} created (always version 1) + * @throws DuplicateSecretException if the secret already exists locally + * @throws QuorumNotReachedException if not enough peers acknowledged the prepare + * @throws IllegalArgumentException if key or value is null/blank + */ public SecretVersion postAcrossCluster(SecretKey key, String value) { validateInput(key, value); if (secretPartRepository.exists(key)) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPutService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPutService.java index ae0019f..0795f91 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPutService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalPutService.java @@ -22,6 +22,21 @@ import edu.yu.capstone.DistributedSecretsVault.service.internal.NodeClient.PeerResponse; import edu.yu.capstone.DistributedSecretsVault.service.secret.SecretSharingService; +/** + * Orchestrates the distributed two-phase commit for updating an existing secret. + *

+ * Flow: + *

    + *
  1. Read the current version from local Redis
  2. + *
  3. Split the new plaintext into Shamir shards with version = current + 1
  4. + *
  5. Buffer the local shard and broadcast prepare requests to peers
  6. + *
  7. If quorum is reached, publish a commit message via Kafka
  8. + *
  9. If quorum fails, discard the buffered action
  10. + *
+ * + * @see PutPrepareHandler + * @see PutCommitHandler + */ @Service public class InternalPutService { private static final Logger log = LoggerFactory.getLogger(InternalPutService.class); @@ -55,6 +70,16 @@ public InternalPutService(NodeClient nodeClient, ? envNodeId : "local-node"; } + /** + * Updates a secret across the cluster using the two-phase commit protocol. + * + * @param key composite key identifying the secret + * @param value new plaintext value + * @return the {@link SecretVersion} with the new version number + * @throws SecretNotFoundException if the secret does not exist locally + * @throws QuorumNotReachedException if not enough peers acknowledged the prepare + * @throws IllegalArgumentException if key or value is null/blank + */ public SecretVersion putAcrossCluster(SecretKey key, String value) { validateInput(key, value); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalRepairService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalRepairService.java index 985c156..70a9178 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalRepairService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/InternalRepairService.java @@ -18,6 +18,21 @@ import edu.yu.capstone.DistributedSecretsVault.service.internal.NodeClient.PeerResponse; import edu.yu.capstone.DistributedSecretsVault.service.secret.SecretSharingService; +/** + * Orchestrates the read-repair protocol to restore full shard redundancy. + *

+ * Read-repair is triggered automatically during a read when the number of + * available shards is near (or at) the reconstruction threshold. The service + * re-splits the plaintext into fresh Shamir shards and distributes them to + * nodes that are missing the latest version, using the same two-phase + * commit protocol as POST/PUT. + *

+ * The repair is "best-effort": if quorum cannot be reached or the Kafka + * commit fails, the repair is silently skipped without affecting the read. + * + * @see RepairPrepareHandler + * @see RepairCommitHandler + */ @Service public class InternalRepairService { private static final Logger log = LoggerFactory.getLogger(InternalRepairService.class); @@ -48,6 +63,20 @@ public InternalRepairService(NodeClient nodeClient, ? envNodeId : "local-node"; } + /** + * Determines whether a read-repair should be triggered after a successful read. + *

+ * Returns {@code true} when: + *

+ * + * @param availableParts number of shards successfully collected + * @param liveRepairTargets number of live nodes that were missing the shard + * @return {@code true} if repair should be attempted + */ public boolean shouldRepairLatestRead(int availableParts, int liveRepairTargets) { if (clusterConfig == null || !clusterConfig.isRepairEnabled()) { return false; @@ -64,6 +93,18 @@ public boolean shouldRepairLatestRead(int availableParts, int liveRepairTargets) return availableParts <= threshold + buffer; } + /** + * Executes a read-repair for the specified secret version. + *

+ * Re-splits the value into Shamir shards and distributes them to all nodes + * using the two-phase commit protocol. If quorum is not reached or the + * Kafka commit fails, the repair is silently abandoned. + * + * @param key the composite secret key + * @param version the version number to repair + * @param value the reconstructed plaintext value to re-shard + * @throws IllegalArgumentException if key or value is null + */ public void repairLatestVersion(SecretKey key, long version, String value) { if (key == null || value == null) { throw new IllegalArgumentException("Secret key and value are required for repair"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/NodeClient.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/NodeClient.java index d2280bd..baccad0 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/NodeClient.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/NodeClient.java @@ -44,6 +44,15 @@ public class NodeClient { private final String selfHost; private final int httpPort; + /** + * Constructs the node client with a REST client and optional ScaleCube instance. + *

+ * Reads {@code POD_IP} for self-identification and {@code SERVER_PORT} + * for the HTTP port, falling back to {@code localhost} and {@code 8080}. + * + * @param restClient configured REST client for outbound HTTP calls + * @param microservices optional ScaleCube instance for peer discovery + */ public NodeClient(RestClient restClient, Optional microservices) { this.restClient = restClient; this.microservices = microservices; @@ -89,6 +98,13 @@ public PeerResponse sendDeletePrepare(String peerUrl, DeletePrepareRequest reque } } + /** + * Sends a post-prepare request to a peer node. + * + * @param peerUrl base URL of the peer + * @param request the prepare request containing the shard + * @return peer response indicating ACK or failure + */ public PeerResponse sendPostPrepare(String peerUrl, PostPrepareRequest request) { try { restClient.post() @@ -107,6 +123,13 @@ public PeerResponse sendPostPrepare(String peerUrl, PostPrepareRequest request) } } + /** + * Sends a put-prepare request to a peer node. + * + * @param peerUrl base URL of the peer + * @param request the prepare request containing the updated shard + * @return peer response indicating ACK or failure + */ public PeerResponse sendPutPrepare(String peerUrl, PutPrepareRequest request) { try { restClient.put() @@ -125,6 +148,13 @@ public PeerResponse sendPutPrepare(String peerUrl, PutPrepareRequest request) { } } + /** + * Sends a repair-prepare request to a peer node. + * + * @param peerUrl base URL of the peer + * @param request the repair prepare request containing the shard + * @return peer response indicating ACK or failure + */ public PeerResponse sendRepairPrepare(String peerUrl, RepairPrepareRequest request) { try { restClient.post() @@ -143,6 +173,14 @@ public PeerResponse sendRepairPrepare(String peerUrl, RepairPrepareRequest reque } } + /** + * Fetches a single secret shard from a peer node, optionally at a specific version. + * + * @param peerUrl base URL of the peer + * @param key the secret key to fetch + * @param version optional version; if null, fetches the latest + * @return response containing the shard if found, or error details + */ public SecretPartResponse fetchSecretPart(String peerUrl, SecretKey key, Long version) { try { SecretPart part; @@ -173,6 +211,13 @@ public SecretPartResponse fetchSecretPart(String peerUrl, SecretKey key, Long ve } } + /** + * Fetches all version shards for a secret from a peer node. + * + * @param peerUrl base URL of the peer + * @param key the secret key to fetch + * @return response containing a map of version→shard if found, or error details + */ public SecretPartsResponse fetchAllSecretParts(String peerUrl, SecretKey key) { try { Map parts = restClient.get() @@ -247,6 +292,7 @@ private static String extractHost(String address) { return address; } + /** Reads a value from environment variables, falling back to system properties. */ private static String readEnv(String key) { String value = System.getenv(key); if (value == null || value.isBlank()) { @@ -255,6 +301,14 @@ private static String readEnv(String key) { return value; } + /** + * Result of a prepare request to a peer node. + * + * @param peerUrl the peer's base URL + * @param acknowledged {@code true} if the peer accepted the prepare + * @param statusCode HTTP status code (null if network failure) + * @param errorMessage error detail (null on success) + */ public record PeerResponse(String peerUrl, boolean acknowledged, Integer statusCode, String errorMessage) { public static PeerResponse acknowledged(String peerUrl) { return new PeerResponse(peerUrl, true, null, null); @@ -269,6 +323,14 @@ public static PeerResponse failed(String peerUrl, String errorMessage) { } } + /** + * Result of a single-shard fetch from a peer node. + * + * @param peerUrl the peer's base URL + * @param part the fetched shard (null if not found or on error) + * @param statusCode HTTP status code (null if network failure) + * @param errorMessage error detail (null on success) + */ public record SecretPartResponse(String peerUrl, SecretPart part, Integer statusCode, String errorMessage) { public boolean found() { return part != null; @@ -287,6 +349,14 @@ public static SecretPartResponse failed(String peerUrl, String errorMessage) { } } + /** + * Result of an all-versions fetch from a peer node. + * + * @param peerUrl the peer's base URL + * @param parts map of version→shard (null or empty if not found) + * @param statusCode HTTP status code (null if network failure) + * @param errorMessage error detail (null on success) + */ public record SecretPartsResponse(String peerUrl, Map parts, Integer statusCode, String errorMessage) { public boolean found() { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PendingActionsBuffer.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PendingActionsBuffer.java index a2628d4..675ed1c 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PendingActionsBuffer.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PendingActionsBuffer.java @@ -49,6 +49,11 @@ public class PendingActionsBuffer { private final Map locksBySecretKey = new ConcurrentHashMap<>(); private final long evictionTimeoutMillis; + /** + * Constructs the buffer with the eviction timeout from cluster configuration. + * + * @param clusterConfig cluster config providing {@link ClusterConfig#getLockTimeoutMillis()} + */ public PendingActionsBuffer(ClusterConfig clusterConfig) { long timeout = clusterConfig.getLockTimeoutMillis(); this.evictionTimeoutMillis = timeout > 0 ? timeout : 30_000L; @@ -65,6 +70,14 @@ public void bufferAction(UUID operationId, SecretKey secretKey, ActionType actio bufferAction(operationId, secretKey, actionType, null); } + /** + * Buffer an action received during the prepare phase, including a shard payload. + * + * @param operationId unique ID correlating prepare to commit + * @param secretKey the key being affected by this action + * @param actionType the type of action being buffered + * @param secretPart the shard payload to buffer (null for delete operations) + */ public void bufferAction(UUID operationId, SecretKey secretKey, ActionType actionType, SecretPart secretPart) { PendingAction entry = new PendingAction(operationId, secretKey, actionType, secretPart, Instant.now()); KeyLock keyLock = acquireLock(secretKey); @@ -129,6 +142,15 @@ public PendingAction commitAndRemove(UUID operationId) { } } + /** + * Discard a buffered action without committing it. + *

+ * Used when the originator determines that quorum was not reached + * and the operation should be abandoned. + * + * @param operationId the operation ID to discard + * @return the discarded pending action, or {@code null} if not found + */ public PendingAction discard(UUID operationId) { PendingAction candidate = byOperationId.get(operationId); if (candidate == null) { @@ -214,6 +236,7 @@ public void evictExpired() { } } + /** Removes an operation ID from the secondary (SecretKey → operationIds) index. */ private void removeFromSecretKeyIndex(SecretKey key, UUID operationId) { Set ops = bySecretKey.get(key); if (ops != null) { @@ -224,6 +247,10 @@ private void removeFromSecretKeyIndex(SecretKey key, UUID operationId) { } } + /** + * Acquires the per-key reentrant lock, creating it if necessary. + * Uses reference counting so locks can be cleaned up when no longer needed. + */ private KeyLock acquireLock(SecretKey key) { KeyLock keyLock = locksBySecretKey.compute(key, (ignored, existing) -> { if (existing == null) { @@ -236,6 +263,10 @@ private KeyLock acquireLock(SecretKey key) { return keyLock; } + /** + * Releases the per-key lock and removes it from the map if no other + * threads hold a reference. + */ private void releaseLock(SecretKey key, KeyLock keyLock) { try { keyLock.unlock(); @@ -249,6 +280,14 @@ private void releaseLock(SecretKey key, KeyLock keyLock) { } } + /** + * Reference-counted reentrant lock for per-key synchronization. + *

+ * Each {@link SecretKey} gets its own lock so that operations on different + * secrets do not contend with each other. The reference count tracks how + * many threads currently hold or are waiting for this lock; when it drops + * to zero, the lock is removed from the map. + */ private static final class KeyLock { private final ReentrantLock lock = new ReentrantLock(); private final AtomicInteger references = new AtomicInteger(1); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostCommitHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostCommitHandler.java index 0ef1cc8..e8614f4 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostCommitHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostCommitHandler.java @@ -8,6 +8,19 @@ import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; import edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer.PendingAction; +/** + * Handles incoming commit requests for distributed create (POST) operations. + *

+ * When the commit message arrives via Kafka, this handler: + *

    + *
  1. Retrieves and removes the buffered prepare action
  2. + *
  3. Verifies the action type and secret key match
  4. + *
  5. Re-checks uniqueness (idempotency guard)
  6. + *
  7. Persists the shard via {@link SecretPartRepository#savePart}
  8. + *
+ * + * @see PostPrepareHandler + */ @Service public class PostCommitHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -19,6 +32,14 @@ public PostCommitHandler(PendingActionsBuffer pendingActionsBuffer, this.secretPartRepository = secretPartRepository; } + /** + * Processes a post-commit request: finalizes the buffered create action. + * + * @param request the commit request from Kafka + * @throws IllegalArgumentException if the request is invalid + * @throws InternalOperationConflictException if the buffered action is missing or mismatched + * @throws DuplicateSecretException if the secret now exists (concurrent create) + */ public void handle(PostCommitRequest request) { validateRequest(request); @@ -46,6 +67,7 @@ public void handle(PostCommitRequest request) { secretPartRepository.savePart(committed.secretPart()); } + /** Validates that all required fields of a post-commit request are present. */ private void validateRequest(PostCommitRequest request) { if (request == null) { throw new IllegalArgumentException("Post commit request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostPrepareHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostPrepareHandler.java index aec3285..2d73b20 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostPrepareHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PostPrepareHandler.java @@ -8,6 +8,19 @@ import edu.yu.capstone.DistributedSecretsVault.exceptions.DuplicateSecretException; import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; +/** + * Handles incoming prepare requests for distributed create (POST) operations. + *

+ * When the originator broadcasts a post-prepare, each peer: + *

    + *
  1. Validates the request
  2. + *
  3. Checks that the secret does not already exist locally (duplicate check)
  4. + *
  5. Buffers the shard in {@link PendingActionsBuffer}
  6. + *
+ * The originator interprets a successful return (no exception) as an ACK. + * + * @see PostCommitHandler + */ @Service public class PostPrepareHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -19,6 +32,13 @@ public PostPrepareHandler(PendingActionsBuffer pendingActionsBuffer, this.secretPartRepository = secretPartRepository; } + /** + * Processes a post-prepare request: verifies uniqueness and buffers the shard. + * + * @param request the prepare request from the originator + * @throws IllegalArgumentException if the request is invalid + * @throws DuplicateSecretException if the secret already exists locally + */ public void handle(PostPrepareRequest request) { validateRequest(request); @@ -36,6 +56,7 @@ public void handle(PostPrepareRequest request) { request.getOperationId(), message.getKey(), ActionType.POST, part); } + /** Validates that all required fields of a post-prepare request are present. */ private void validateRequest(PostPrepareRequest request) { if (request == null) { throw new IllegalArgumentException("Post prepare request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutCommitHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutCommitHandler.java index 4da2345..7214275 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutCommitHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutCommitHandler.java @@ -8,6 +8,18 @@ import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; import edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer.PendingAction; +/** + * Handles incoming commit requests for distributed update (PUT) operations. + *

+ * When the commit message arrives via Kafka, this handler: + *

    + *
  1. Retrieves and removes the buffered prepare action
  2. + *
  3. Verifies the action type and secret key match
  4. + *
  5. Updates the shard via {@link SecretPartRepository#updatePart}
  6. + *
+ * + * @see PutPrepareHandler + */ @Service public class PutCommitHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -19,6 +31,14 @@ public PutCommitHandler(PendingActionsBuffer pendingActionsBuffer, this.secretPartRepository = secretPartRepository; } + /** + * Processes a put-commit request: finalizes the buffered update action. + * + * @param request the commit request from Kafka + * @throws IllegalArgumentException if the request is invalid + * @throws InternalOperationConflictException if the buffered action is missing or mismatched + * @throws SecretNotFoundException if the secret no longer exists + */ public void handle(PutCommitRequest request) { validateRequest(request); @@ -45,6 +65,7 @@ public void handle(PutCommitRequest request) { } } + /** Validates that all required fields of a put-commit request are present. */ private void validateRequest(PutCommitRequest request) { if (request == null) { throw new IllegalArgumentException("Put commit request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutPrepareHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutPrepareHandler.java index 2a28ab2..c990e02 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutPrepareHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/PutPrepareHandler.java @@ -8,6 +8,19 @@ import edu.yu.capstone.DistributedSecretsVault.exceptions.SecretNotFoundException; import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; +/** + * Handles incoming prepare requests for distributed update (PUT) operations. + *

+ * When the originator broadcasts a put-prepare, each peer: + *

    + *
  1. Validates the request
  2. + *
  3. Checks that the secret does exist locally
  4. + *
  5. Buffers the updated shard in {@link PendingActionsBuffer}
  6. + *
+ * The originator interprets a successful return (no exception) as an ACK. + * + * @see PutCommitHandler + */ @Service public class PutPrepareHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -19,6 +32,13 @@ public PutPrepareHandler(PendingActionsBuffer pendingActionsBuffer, this.secretPartRepository = secretPartRepository; } + /** + * Processes a put-prepare request: verifies existence and buffers the updated shard. + * + * @param request the prepare request from the originator + * @throws IllegalArgumentException if the request is invalid + * @throws SecretNotFoundException if the secret does not exist locally + */ public void handle(PutPrepareRequest request) { validateRequest(request); @@ -36,6 +56,7 @@ public void handle(PutPrepareRequest request) { request.getOperationId(), message.getKey(), ActionType.PUT, part); } + /** Validates that all required fields of a put-prepare request are present. */ private void validateRequest(PutPrepareRequest request) { if (request == null) { throw new IllegalArgumentException("Put prepare request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairCommitHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairCommitHandler.java index 7c5dbd8..5992e37 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairCommitHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairCommitHandler.java @@ -8,6 +8,20 @@ import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; import edu.yu.capstone.DistributedSecretsVault.service.internal.PendingActionsBuffer.PendingAction; +/** + * Handles incoming commit requests for read-repair operations. + *

+ * When the commit message arrives via Kafka, this handler: + *

    + *
  1. Retrieves and removes the buffered repair action
  2. + *
  3. Verifies the action type and secret key match
  4. + *
  5. Saves the repair shard via {@link SecretPartRepository#savePart}
  6. + *
+ * Repair commits always use {@code savePart} (not {@code updatePart}) + * because the shard may not previously exist on this node. + * + * @see RepairPrepareHandler + */ @Service public class RepairCommitHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -19,6 +33,13 @@ public RepairCommitHandler(PendingActionsBuffer pendingActionsBuffer, this.secretPartRepository = secretPartRepository; } + /** + * Processes a repair-commit request: finalizes the buffered repair action. + * + * @param request the commit request from Kafka + * @throws IllegalArgumentException if the request is invalid + * @throws InternalOperationConflictException if the buffered action is missing or mismatched + */ public void handle(RepairCommitRequest request) { validateRequest(request); @@ -44,6 +65,7 @@ public void handle(RepairCommitRequest request) { secretPartRepository.savePart(part); } + /** Validates that all required fields of a repair-commit request are present. */ private void validateRequest(RepairCommitRequest request) { if (request == null) { throw new IllegalArgumentException("Repair commit request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairPrepareHandler.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairPrepareHandler.java index 28803a1..579767e 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairPrepareHandler.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/internal/RepairPrepareHandler.java @@ -6,6 +6,16 @@ import edu.yu.capstone.DistributedSecretsVault.dto.internal.RepairPrepareRequest; import edu.yu.capstone.DistributedSecretsVault.dto.internal.SecretPartMessage; +/** + * Handles incoming prepare requests for read-repair operations. + *

+ * Unlike POST/PUT, repair does not check for existence — the shard may + * not exist on this node (that's the whole point of the repair). The handler + * simply buffers the shard in {@link PendingActionsBuffer} for later commit. + * + * @see RepairCommitHandler + * @see InternalRepairService + */ @Service public class RepairPrepareHandler { private final PendingActionsBuffer pendingActionsBuffer; @@ -14,6 +24,12 @@ public RepairPrepareHandler(PendingActionsBuffer pendingActionsBuffer) { this.pendingActionsBuffer = pendingActionsBuffer; } + /** + * Processes a repair-prepare request: buffers the shard without existence checks. + * + * @param request the repair prepare request from the originator + * @throws IllegalArgumentException if the request is invalid + */ public void handle(RepairPrepareRequest request) { validateRequest(request); @@ -27,6 +43,7 @@ public void handle(RepairPrepareRequest request) { request.getOperationId(), message.getKey(), ActionType.REPAIR, part); } + /** Validates that all required fields of a repair-prepare request are present. */ private void validateRequest(RepairPrepareRequest request) { if (request == null) { throw new IllegalArgumentException("Repair prepare request is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/DeleteSecretService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/DeleteSecretService.java index 4be15ee..a1cdf91 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/DeleteSecretService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/DeleteSecretService.java @@ -9,16 +9,34 @@ import edu.yu.capstone.DistributedSecretsVault.dto.secret.DeleteSecretRequest; import edu.yu.capstone.DistributedSecretsVault.service.internal.InternalDeleteService; +/** + * Public-facing command service for deleting a secret. + *

+ * The service validates the request, converts it to a {@link SecretKey}, and + * delegates the distributed delete workflow to {@link InternalDeleteService}. + */ @Service public class DeleteSecretService implements SecretCommand { private static final Logger log = LoggerFactory.getLogger(DeleteSecretService.class); private final InternalDeleteService internalDeleteService; + /** + * Creates a delete command backed by the distributed internal delete service. + * + * @param internalDeleteService service that deletes secret shards across the cluster + */ public DeleteSecretService(InternalDeleteService internalDeleteService) { this.internalDeleteService = internalDeleteService; } + /** + * Deletes all versions and shards for the requested secret. + * + * @param input delete request containing user and secret name + * @return HTTP 204 response when the delete commit has been submitted + * @throws IllegalArgumentException if the request or user is missing + */ @Override public ResponseEntity execute(DeleteSecretRequest input) { if (input == null) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/EnvFileService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/EnvFileService.java index bcc6e15..59807e3 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/EnvFileService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/EnvFileService.java @@ -21,6 +21,15 @@ import edu.yu.capstone.DistributedSecretsVault.exceptions.SecretNotFoundException; import edu.yu.capstone.DistributedSecretsVault.repository.SecretPartRepository; +/** + * Applies a batch of secret operations encoded in a {@code .env}-style file. + *

+ * Supported entries are {@code KEY=new:value}, {@code KEY=update:value}, + * {@code KEY=get}, {@code KEY=get:version}, and {@code KEY=delete}. Blank + * lines and comment lines are ignored. Create, update, and get operations are + * returned as regular {@code KEY=value} lines so callers can materialize a + * resolved environment file. + */ @Service public class EnvFileService { private static final Logger log = LoggerFactory.getLogger(EnvFileService.class); @@ -31,6 +40,16 @@ public class EnvFileService { private final GetSecretService getSecretService; private final SecretPartRepository secretPartRepository; + /** + * Creates a batch processor that delegates each parsed operation to the + * existing single-secret services. + * + * @param postSecretService service used for {@code new} operations + * @param putSecretService service used for {@code update} operations + * @param deleteSecretService service used for {@code delete} operations + * @param getSecretService service used for {@code get} operations + * @param secretPartRepository repository used for existence checks + */ public EnvFileService(PostSecretService postSecretService, PutSecretService putSecretService, DeleteSecretService deleteSecretService, @@ -43,6 +62,20 @@ public EnvFileService(PostSecretService postSecretService, this.secretPartRepository = secretPartRepository; } + /** + * Parses, validates, and executes all operations in the submitted file. + *

+ * Preconditions are checked for every operation before any write/delete is + * performed, which keeps malformed batches from being partially applied. + * + * @param user owner whose secrets should be modified or read + * @param envFileContent raw {@code .env}-style content + * @return HTTP 200 response containing output {@code KEY=value} lines + * @throws IllegalArgumentException if the user or file content is invalid + * @throws DuplicateSecretException if a {@code new} operation targets an existing secret + * @throws SecretNotFoundException if an {@code update}, {@code get}, or {@code delete} + * operation targets a missing secret + */ public ResponseEntity execute(String user, String envFileContent) { validateUser(user); List operations = parseOperations(envFileContent); @@ -81,12 +114,16 @@ public ResponseEntity execute(String user, String envFileContent) { return ResponseEntity.ok(String.join("\n", resultLines)); } + /** Ensures every batch is scoped to a concrete secret owner. */ private void validateUser(String user) { if (user == null || user.isBlank()) { throw new IllegalArgumentException("User is required"); } } + /** + * Converts file lines into typed operations while rejecting duplicate keys. + */ private List parseOperations(String envFileContent) { if (envFileContent == null || envFileContent.isBlank()) { throw new IllegalArgumentException(".env file content is required"); @@ -102,6 +139,7 @@ private List parseOperations(String envFileContent) { continue; } + // Enforce one operation per key so a batch has deterministic behavior. EnvSecretOperation operation = parseOperationLine(trimmedLine, index + 1); if (!seenKeys.add(operation.key())) { throw new IllegalArgumentException("Duplicate key in .env file: " + operation.key()); @@ -115,6 +153,10 @@ private List parseOperations(String envFileContent) { return operations; } + /** + * Parses a single non-comment entry into its secret key, action, value, and + * optional version. + */ private EnvSecretOperation parseOperationLine(String line, int lineNumber) { int equalsIndex = line.indexOf('='); if (equalsIndex <= 0) { @@ -144,6 +186,7 @@ private EnvSecretOperation parseOperationLine(String line, int lineNumber) { + ": expected new, update, get, or delete"); }; + // Values are required only for write actions; get/delete may be bare actions. if (colonIndex < 0 && action != EnvAction.GET && action != EnvAction.DELETE) { throw invalidLine(lineNumber); } @@ -155,6 +198,7 @@ private EnvSecretOperation parseOperationLine(String line, int lineNumber) { return new EnvSecretOperation(key, action, value, version); } + /** Parses the optional version suffix used by {@code KEY=get:version}. */ private Long parseGetVersion(String value, int lineNumber) { String trimmedValue = value.trim(); if (trimmedValue.isEmpty()) { @@ -171,11 +215,18 @@ private Long parseGetVersion(String value, int lineNumber) { } } + /** Creates a consistent validation error for malformed .env entries. */ private IllegalArgumentException invalidLine(int lineNumber) { return new IllegalArgumentException("Invalid .env entry on line " + lineNumber + ": expected KEY=new:value, KEY=update:value, KEY=get, KEY=get:version, or KEY=delete"); } + /** + * Verifies all operation preconditions before mutating cluster state. + * + * @return cached values for {@code get} operations so execution does not + * perform a second distributed read + */ private Map validateOperationPreconditions(String user, List operations) { Map getResults = new HashMap<>(); for (EnvSecretOperation operation : operations) { @@ -200,10 +251,12 @@ private Map validateOperationPreconditions(String user, List + * This class validates controller input, builds the canonical {@link SecretKey}, + * and delegates cluster-aware retrieval to {@link InternalGetService}. + */ @Service public class GetSecretService { private static final Logger log = LoggerFactory.getLogger(GetSecretService.class); private final InternalGetService internalGetService; + /** + * Creates a get service backed by the distributed internal read service. + * + * @param internalGetService service that gathers shards across the cluster + */ public GetSecretService(InternalGetService internalGetService) { this.internalGetService = internalGetService; } + /** + * Retrieves one version of a secret for a user. + * + * @param user owner of the secret + * @param secretName human-readable secret name + * @param version version to retrieve, or {@code null} for the latest version + * @return HTTP 200 response containing the reconstructed plaintext secret + * @throws IllegalArgumentException if the user or secret name is blank + */ public ResponseEntity getVersion(String user, String secretName, Long version) { SecretKey key = validate(user, secretName); log.info("Retrieve secret requested: user={}, secretName={}, version={}", @@ -30,6 +50,14 @@ public ResponseEntity getVersion(String user, String secretName, Long ve return ResponseEntity.ok(secretValue); } + /** + * Retrieves every stored version of a secret for a user. + * + * @param user owner of the secret + * @param secretName human-readable secret name + * @return HTTP 200 response mapping version numbers to plaintext values + * @throws IllegalArgumentException if the user or secret name is blank + */ public ResponseEntity> getAllVersions(String user, String secretName) { SecretKey key = validate(user, secretName); log.info("Retrieve all secret versions requested: user={}, secretName={}", @@ -40,6 +68,7 @@ public ResponseEntity> getAllVersions(String user, String secr return ResponseEntity.ok(versions); } + /** Validates request identifiers before constructing the immutable domain key. */ private SecretKey validate(String user, String secretName) { if (user == null || user.isBlank()) { throw new IllegalArgumentException("User is required"); diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PostSecretService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PostSecretService.java index 6b3f8f0..759e69f 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PostSecretService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PostSecretService.java @@ -11,16 +11,34 @@ import edu.yu.capstone.DistributedSecretsVault.dto.secret.PostSecretRequest; import edu.yu.capstone.DistributedSecretsVault.service.internal.InternalPostService; +/** + * Public-facing command service for creating a new secret. + *

+ * This layer performs request validation and response formatting while + * {@link InternalPostService} owns the cluster coordination and shard storage. + */ @Service public class PostSecretService implements SecretCommand { private static final Logger log = LoggerFactory.getLogger(PostSecretService.class); private final InternalPostService internalPostService; + /** + * Creates a post command backed by the distributed internal create service. + * + * @param internalPostService service that creates secret shards across the cluster + */ public PostSecretService(InternalPostService internalPostService) { this.internalPostService = internalPostService; } + /** + * Creates version 1 of a secret for the given user. + * + * @param input create request containing user, name, and plaintext value + * @return HTTP 201 response with the created version number + * @throws IllegalArgumentException if the request or user is missing + */ @Override public ResponseEntity execute(PostSecretRequest input) { if (input == null) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PutSecretService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PutSecretService.java index f017b6a..ee3c4bc 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PutSecretService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/PutSecretService.java @@ -10,16 +10,34 @@ import edu.yu.capstone.DistributedSecretsVault.domain.model.SecretVersion; import edu.yu.capstone.DistributedSecretsVault.service.internal.InternalPutService; +/** + * Public-facing command service for updating an existing secret. + *

+ * The service validates the request, builds the secret key, and delegates + * version creation and cluster coordination to {@link InternalPutService}. + */ @Service public class PutSecretService implements SecretCommand { private static final Logger log = LoggerFactory.getLogger(PutSecretService.class); private final InternalPutService internalPutService; + /** + * Creates an update command backed by the distributed internal put service. + * + * @param internalPutService service that writes the next secret version across the cluster + */ public PutSecretService(InternalPutService internalPutService) { this.internalPutService = internalPutService; } + /** + * Stores a new version for an existing secret. + * + * @param input update request containing user, secret name, and new plaintext value + * @return HTTP 200 response with the new version number + * @throws IllegalArgumentException if the request or user is missing + */ @Override public ResponseEntity execute(PutSecretRequest input) { if (input == null) { diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretCommand.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretCommand.java index c387076..7d3198d 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretCommand.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretCommand.java @@ -2,6 +2,19 @@ import org.springframework.http.ResponseEntity; +/** + * Generic command contract for secret operations exposed through service classes. + * + * @param request type accepted by the command + * @param response body type produced by the command + */ public interface SecretCommand { + /** + * Executes a secret operation and returns the HTTP response expected by the + * controller layer. + * + * @param input operation request + * @return response entity containing the command result + */ public ResponseEntity execute(I input); } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretReconstructionService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretReconstructionService.java index ee63ed6..1c578cd 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretReconstructionService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretReconstructionService.java @@ -11,16 +11,32 @@ import edu.yu.capstone.DistributedSecretsVault.encrypt.SecretReconstructor; import edu.yu.capstone.DistributedSecretsVault.exceptions.InsufficientShardsException; +/** + * Reassembles plaintext secret values from collected Shamir shards. + *

+ * Internal read services provide the available {@link SecretPart} records, and + * this adapter converts them into the share map expected by + * {@link SecretReconstructor}. + */ @Service public class SecretReconstructionService { + /** Stateless wrapper around the Shamir reconstruction implementation. */ private final SecretReconstructor secretReconstructor = new SecretReconstructor(); + /** + * Reconstructs a plaintext secret from its available shard records. + * + * @param parts shard records gathered from local storage and peers + * @return reconstructed plaintext secret value + * @throws InsufficientShardsException if no usable shards are provided + */ public String reconstruct(List parts) { if (parts == null || parts.isEmpty()) { throw new InsufficientShardsException(); } Map partMap = new HashMap<>(); for (SecretPart part : parts) { + // Ignore null records so callers can pass partially populated peer responses. if (part == null || part.getShard() == null) { continue; } diff --git a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretSharingService.java b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretSharingService.java index 4844fb6..2f2a7f9 100644 --- a/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretSharingService.java +++ b/src/main/java/edu/yu/capstone/DistributedSecretsVault/service/secret/SecretSharingService.java @@ -11,10 +11,27 @@ import edu.yu.capstone.DistributedSecretsVault.domain.model.SecretPart; import edu.yu.capstone.DistributedSecretsVault.encrypt.SecretSplitter; +/** + * Converts plaintext secret values into Shamir secret-sharing shards. + *

+ * Internal write services use this adapter to transform a user value into + * {@link SecretPart} domain objects that can be distributed across cluster nodes. + */ @Service public class SecretSharingService { + /** Stateless wrapper around the Shamir splitting implementation. */ private final SecretSplitter secretSplitter = new SecretSplitter(); + /** + * Splits a plaintext secret value into domain shard objects. + * + * @param key composite key identifying the secret being split + * @param value plaintext secret value + * @param threshold minimum number of shards required to reconstruct the value + * @param totalParts total number of shards to generate + * @return list of secret parts with the supplied key and generated shard payloads + * @throws IllegalArgumentException if the key/name or value is missing + */ public List split(SecretKey key, String value, int threshold, int totalParts) { if (key == null || key.getName() == null || key.getName().isBlank()) { throw new IllegalArgumentException("Secret key is required"); @@ -25,6 +42,7 @@ public List split(SecretKey key, String value, int threshold, int to Map parts = secretSplitter.split(value.getBytes(StandardCharsets.UTF_8), totalParts, threshold); List secretParts = new ArrayList<>(parts.size()); for (Map.Entry entry : parts.entrySet()) { + // Preserve the Shamir share index so reconstruction can identify each point. SecretPart part = new SecretPart(); part.setKey(key); part.setPartIndex(entry.getKey());