diff --git a/flowvault/pom.xml b/flowvault/pom.xml index 854d13d0..7c918cb2 100644 --- a/flowvault/pom.xml +++ b/flowvault/pom.xml @@ -11,7 +11,7 @@ skyflow-flowvault-java - 3.0.0-beta.13-dev.9a074a90 + 3.0.0-beta.13-dev.3671a19a jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java new file mode 100644 index 00000000..48889218 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java @@ -0,0 +1,88 @@ +package com.example.serviceaccount; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeData; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; +import io.github.cdimascio.dotenv.Dotenv; +import java.util.ArrayList; + +/** + * This example demonstrates how to configure and use the Skyflow SDK + * to detokenize sensitive data stored in a Skyflow vault. + * It includes setting up credentials, configuring the vault, and + * making a detokenization request. The code also implements a retry + * mechanism to handle unauthorized access errors (HTTP 401). + */ +public class BearerTokenExpiryExample { + public static void main(String[] args) { + try { + // Setting up credentials for accessing the Skyflow vault + Credentials vaultCredentials = new Credentials(); + vaultCredentials.setCredentialsString(""); + + // Configuring the Skyflow vault with necessary details + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); // Vault ID + vaultConfig.setClusterId(""); // Cluster ID + vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) + vaultConfig.setCredentials(vaultCredentials); // Setting credentials + + // Creating a Skyflow client instance with the configured vault + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR + .addVaultConfig(vaultConfig) // Adding vault configuration + .build(); + + // Attempting to detokenize data using the Skyflow client + try { + detokenizeData(skyflowClient); + } catch (SkyflowException e) { + // Retry detokenization if the error is due to unauthorized access (HTTP 401) + if (e.getHttpCode() == 401) { + detokenizeData(skyflowClient); + } else { + // Rethrow the exception for other error codes + throw e; + } + } + } catch (SkyflowException e) { + // Handling any exceptions that occur during the process + System.out.println("An error occurred: " + e.getMessage()); + } + } + + /** + * Method to detokenize data using the Skyflow client. + * It sends a detokenization request with a list of tokens and prints the response. + * + * @param skyflowClient The Skyflow client instance used for detokenization. + * @throws SkyflowException If an error occurs during the detokenization process. + */ + public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { + // Creating a list of tokens to be detokenized + DetokenizeData detokenizeDataToken1 = new DetokenizeData("", RedactionType.PLAIN_TEXT); + DetokenizeData detokenizeDataToken2 = new DetokenizeData(""); + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(detokenizeDataToken1); // First token + detokenizeDataList.add(detokenizeDataToken2); // Second token + + // Building a detokenization request with the token list and configuration + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) // Adding tokens to the request + .continueOnError(false) // Stop on error + .build(); + + // Sending the detokenization request and receiving the response + DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); + + // Printing the detokenized response + System.out.println(detokenizeResponse); + } +} diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java new file mode 100644 index 00000000..c4578545 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java @@ -0,0 +1,66 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; +import com.skyflow.serviceaccount.util.Token; + +import java.io.File; + +/** + * Example program to generate a Bearer Token using Skyflow's BearerToken utility. + * The token can be generated in two ways: + * 1. Using the file path to a credentials.json file. + * 2. Using the JSON content of the credentials file as a string. + */ +public class BearerTokenGenerationExample { + public static void main(String[] args) { + // Variable to store the generated token + String token = null; + + // Example 1: Generate Bearer Token using a credentials.json file + try { + // Specify the full file path to the credentials.json file + String filePath = ""; + + // Check if the token is either not initialized or has expired + if (Token.isExpired(token)) { + // Create a BearerToken object using the credentials file + BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Set credentials from the file path + .build(); + + // Generate a new Bearer Token + token = bearerToken.getBearerToken(); + } + + // Print the generated Bearer Token to the console + System.out.println("Generated Bearer Token (from file): " + token); + } catch (SkyflowException e) { + // Handle any exceptions encountered during the token generation process + e.printStackTrace(); + } + + // Example 2: Generate Bearer Token using the credentials JSON as a string + try { + // Provide the credentials JSON content as a string + String fileContents = ""; + + // Check if the token is either not initialized or has expired + if (Token.isExpired(token)) { + // Create a BearerToken object using the credentials string + BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Set credentials from the string + .build(); + + // Generate a new Bearer Token + token = bearerToken.getBearerToken(); + } + + // Print the generated Bearer Token to the console + System.out.println("Generated Bearer Token (from string): " + token); + } catch (SkyflowException e) { + // Handle any exceptions encountered during the token generation process + e.printStackTrace(); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java new file mode 100644 index 00000000..30d8f9ba --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java @@ -0,0 +1,72 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; + +/** + * This example demonstrates how to generate Bearer tokens in two different ways: + * 1. Using a credentials file specified by its file path. + * 2. Using the credentials as a string. + *

+ * The code also showcases multithreaded token generation with a shared context (`ctx`), + * where each thread generates and prints tokens repeatedly. + */ +public class BearerTokenGenerationUsingThreadsExample { + public static void main(String[] args) { + // Example 1: Generate Bearer token using a credentials file path + try { + // Step 1: Specify the path to the credentials file + String filePath = ""; // Replace with the actual file path + + // Step 2: Create a BearerToken object using the file path + final BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Provide the credentials file + .setCtx("abc") // Specify a context string ("abc" in this case) + .build(); + + // Step 3: Create and start a thread to repeatedly generate and print tokens + Thread t = new Thread(() -> { + for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times + try { + System.out.println(bearerToken.getBearerToken()); // Print the Bearer token + } catch (SkyflowException e) { // Handle exceptions during token generation + Thread.currentThread().interrupt(); // Interrupt the thread on error + throw new RuntimeException(e); // Wrap and propagate the exception + } + } + }); + t.start(); // Start the thread + } catch (Exception e) { // Handle exceptions during BearerToken creation + e.printStackTrace(); + } + + // Example 2: Generate Bearer token using credentials as a string + try { + // Step 1: Specify the credentials as a string (file contents) + String fileContents = ""; // Replace with actual file contents + + // Step 2: Create a BearerToken object using the credentials string + final BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Provide the credentials as a string + .setCtx("abc") // Specify a context string ("abc" in this case) + .build(); + + // Step 3: Create and start a thread to repeatedly generate and print tokens + Thread t = new Thread(() -> { + for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times + try { + System.out.println(bearerToken.getBearerToken()); // Print the Bearer token + } catch (SkyflowException e) { // Handle exceptions during token generation + Thread.currentThread().interrupt(); // Interrupt the thread on error + throw new RuntimeException(e); // Wrap and propagate the exception + } + } + }); + t.start(); // Start the thread + } catch (Exception e) { // Handle exceptions during BearerToken creation + e.printStackTrace(); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java new file mode 100644 index 00000000..fcb2a407 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java @@ -0,0 +1,73 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * Example program to generate a Bearer Token using Skyflow's BearerToken utility. + * The token is generated using three approaches: + * 1. By providing a string context. + * 2. By providing a JSON object context (Map) for conditional data access policies. + * 3. By providing the credentials as a string with context. + */ +public class BearerTokenGenerationWithContextExample { + public static void main(String[] args) { + String bearerToken = null; + + // Approach 1: Bearer token with string context + // Use a simple string identifier when your policy references a single context value. + try { + String filePath = ""; + BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx("user_12345") + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (string context): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Approach 2: Bearer token with JSON object context + // Use a structured Map when your policy needs multiple context values. + // Each key maps to a Skyflow CEL policy variable under request.context.* + // For example, the map below enables policies like: + // request.context.role == "admin" && request.context.department == "finance" + try { + String filePath = ""; + Map ctx = new HashMap<>(); + ctx.put("role", "admin"); + ctx.put("department", "finance"); + ctx.put("user_id", "user_12345"); + + BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx(ctx) + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (object context): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Approach 3: Bearer token with string context from credentials string + try { + String fileContents = ""; + BearerToken token = BearerToken.builder() + .setCredentials(fileContents) + .setCtx("user_12345") + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (creds string): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java new file mode 100644 index 00000000..3cba8bd5 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java @@ -0,0 +1,66 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; +import java.util.ArrayList; + +/** + * This example demonstrates how to generate a Scoped Bearer Token in two ways: + * 1. Using a credentials file specified by its file path. + * 2. Using the credentials as a string. + *

+ * Scoped tokens are generated by assigning specific roles for access control. + */ +public class ScopedTokenGenerationExample { + public static void main(String[] args) { + String scopedToken = null; // Variable to store the generated Scoped Bearer Token + + // Example 1: Generate Scoped Token using a credentials file path + try { + // Step 1: Specify the roles required for the scoped token + ArrayList roles = new ArrayList<>(); + roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID + + // Step 2: Specify the path to the credentials file + String filePath = ""; // Replace with the actual file path + + // Step 3: Create a BearerToken object using the file path and roles + BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Provide the credentials file + .setRoles(roles) // Set the roles for the scoped token + .build(); + + // Step 4: Generate and print the Scoped Bearer Token + scopedToken = bearerToken.getBearerToken(); + System.out.println("Scoped Token (using file path): " + scopedToken); + } catch (SkyflowException e) { // Handle exceptions during token generation + System.out.println("Error occurred while generating Scoped Token using file path:"); + e.printStackTrace(); + } + + // Example 2: Generate Scoped Token using credentials as a string + try { + // Step 1: Specify the roles required for the scoped token + ArrayList roles = new ArrayList<>(); + roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID + + // Step 2: Specify the credentials as a string (file contents) + String fileContents = ""; // Replace with actual file contents + + // Step 3: Create a BearerToken object using the credentials string and roles + BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Provide the credentials as a string + .setRoles(roles) // Set the roles for the scoped token + .build(); + + // Step 4: Generate and print the Scoped Bearer Token + scopedToken = bearerToken.getBearerToken(); + System.out.println("Scoped Token (using credentials string): " + scopedToken); + } catch (SkyflowException e) { // Handle exceptions during token generation + System.out.println("Error occurred while generating Scoped Token using credentials string:"); + e.printStackTrace(); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java new file mode 100644 index 00000000..517d5cc8 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java @@ -0,0 +1,89 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.SignedDataTokenResponse; +import com.skyflow.serviceaccount.util.SignedDataTokens; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This example demonstrates how to generate Signed Data Tokens using: + * 1. String context. + * 2. JSON object context (Map) for conditional data access policies. + * 3. Credentials string with context. + */ +public class SignedTokenGenerationExample { + public static void main(String[] args) { + List signedTokenValues; + + // Example 1: Signed data tokens with string context + try { + String filePath = ""; + String context = "user_12345"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx(context) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (string context): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Example 2: Signed data tokens with JSON object context + // Each key maps to a Skyflow CEL policy variable under request.context.* + // For example: request.context.role == "analyst" && request.context.department == "research" + try { + String filePath = ""; + Map ctx = new HashMap<>(); + ctx.put("role", "analyst"); + ctx.put("department", "research"); + ctx.put("user_id", "user_67890"); + + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx(ctx) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (object context): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Example 3: Signed data tokens from credentials string + try { + String fileContents = ""; + String context = "user_12345"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(fileContents) + .setCtx(context) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (creds string): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index 6dd6cf29..e4261a00 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -2,9 +2,7 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import com.google.gson.JsonObject; import com.skyflow.config.VaultConfig; @@ -61,12 +59,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Set; public final class Utils extends BaseUtils { @@ -445,7 +439,20 @@ public static List handleBulkInsertBatchException( } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { - BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + String message = null; + if (cause != null && cause.getMessage() != null){ + message = cause.getMessage(); + } + if (cause != null && cause.getLocalizedMessage() !=null) { + message = cause.getLocalizedMessage(); + } + if (cause != null && cause.getCause() !=null) { + message = cause.getCause().toString(); + } + if (message == null || message.isEmpty() || message.trim().isEmpty()){ + message = ex.getMessage(); + } + BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -512,8 +519,21 @@ public static List handleBulkDetokenizeBatchExcept } } else { int indexNumber = batchNumber * batchSize; + String message = null; + if (cause != null && cause.getMessage() != null){ + message = cause.getMessage(); + } + if (cause != null && cause.getLocalizedMessage() !=null) { + message = cause.getLocalizedMessage(); + } + if (cause != null && cause.getCause() !=null) { + message = cause.getCause().toString(); + } + if (message == null || message.isEmpty() || message.trim().isEmpty()){ + message = ex.getMessage(); + } for (int j = 0; j < batch.getTokens().get().size(); j++) { - BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -712,6 +732,10 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { V1RecordResponseObject current = record.get(index); + String reqID = null; + if(current.getError().isPresent()){ + reqID = extractRequestId(headers); + } records.add(new BulkInsertResponseRecord( indexNumber, current.getTableName().orElse(null), @@ -720,7 +744,7 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), - null)); + reqID)); indexNumber++; } formattedResponse = new BulkInsertResponse(records); @@ -736,15 +760,27 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { V1FlowDetokenizeResponseObject current = record.get(index); + Map data = null; + if(current.getMetadata().isPresent()){ + data = current.getMetadata().get(); + if (data.containsKey("skyflowID")) { + Object value = data.remove("skyflowID"); + data.put("skyflowId", value); + } + } + String reqID = null; + if(current.getError().isPresent()){ + reqID = extractRequestId(headers); + } records.add(new BulkDetokenizeResponseRecord( indexNumber, current.getToken().orElse(null), current.getValue().orElse(null), current.getTokenGroupName().orElse(null), - current.getMetadata().orElse(null), + data, current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), - null)); + reqID)); indexNumber++; } return new BulkDetokenizeResponse(records); diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java index 07ed1f47..a3aac9ba 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -47,10 +47,10 @@ import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeOptions; +import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.DeleteTokensOptions; -import com.skyflow.vault.data.DetokenizeOptions; import com.skyflow.vault.data.ErrorRecord; -import com.skyflow.vault.data.InsertOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestContext; import com.skyflow.vault.data.RequestInterceptor; @@ -96,7 +96,7 @@ public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws Sky return bulkInsert(insertRequest, null); } - public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException { + public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); @@ -126,7 +126,7 @@ public CompletableFuture bulkInsertAsync(BulkInsertRequest i return bulkInsertAsync(insertRequest, null); } - public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException { + public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); @@ -164,7 +164,7 @@ public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeReq return bulkDetokenize(detokenizeRequest, null); } - public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { + public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); @@ -191,7 +191,7 @@ public CompletableFuture bulkDetokenizeAsync(BulkDetoken return bulkDetokenizeAsync(detokenizeRequest, null); } - public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { + public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); ExecutorService executor = null; try { diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java new file mode 100644 index 00000000..12d57ca5 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +// Bulk counterpart of DetokenizeOptions. Carries no extra state today; the interceptor +// field is inherited. +public class BulkDetokenizeOptions extends DetokenizeOptions { + + protected BulkDetokenizeOptions(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends DetokenizeOptions.Builder { + + private Builder() { + } + + @Override + public Builder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkDetokenizeOptions build() { + return new BulkDetokenizeOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java new file mode 100644 index 00000000..6dfea493 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +// Bulk counterpart of InsertOptions. Carries no extra state today; the interceptor +// field is inherited. +public class BulkInsertOptions extends InsertOptions { + + protected BulkInsertOptions(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends InsertOptions.Builder { + + private Builder() { + } + + @Override + public Builder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkInsertOptions build() { + return new BulkInsertOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java index 267bd9bd..ddc18bdf 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java @@ -1,9 +1,9 @@ package com.skyflow.vault.data; -public final class DetokenizeOptions { +public class DetokenizeOptions { private final RequestInterceptor interceptor; - private DetokenizeOptions(Builder builder) { + protected DetokenizeOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +15,12 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() { + } + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java index 73262b98..a3d5aa79 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java @@ -1,9 +1,9 @@ package com.skyflow.vault.data; -public final class InsertOptions { +public class InsertOptions { private final RequestInterceptor interceptor; - private InsertOptions(Builder builder) { + protected InsertOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +15,12 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() { + } + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 30697f12..d220084a 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -781,6 +781,37 @@ public void testHandleBulkInsertBatchException_genericExceptionHasNoRequestId() Assert.assertEquals("boom", records.get(0).getError()); } + @Test + public void testHandleBulkInsertBatchException_nonApiCauseUsesCauseMessage() { + // Cause is non-null but not an ApiClientApiException: the message ladder should + // pick up the cause's own message rather than the outer wrapper's. + RuntimeException ex = new RuntimeException("wrapper", new IllegalStateException("inner boom")); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("inner boom", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_nonApiCauseWithNestedCauseUsesNestedToString() { + // When the cause itself wraps another throwable, the ladder resolves the message + // down to the nested cause's toString(). + RuntimeException ex = new RuntimeException("wrapper", + new IllegalStateException("inner boom", new IllegalArgumentException("root cause"))); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError()); + } + // ── createInsertErrorRecord / createDetokenizeErrorRecord branch coverage ─ @Test @@ -1281,6 +1312,24 @@ public void testHandleBulkDetokenizeBatchException_genericException() { Assert.assertEquals("boom", errors.get(0).getError()); } + @Test + public void testHandleBulkDetokenizeBatchException_nonApiCauseUsesCauseMessage() { + // Cause is non-null but not an ApiClientApiException: the message ladder resolves the + // nested cause's toString() rather than the outer wrapper's message. + RuntimeException ex = new RuntimeException("wrapper", + new IllegalStateException("inner boom", new IllegalArgumentException("root cause"))); + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + + List records = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError()); + } + // ── handleBulkDeleteTokensBatchException ────────────────────────────────── @Test @@ -1341,6 +1390,153 @@ public void testHandleBulkDeleteTokensBatchException_genericException() { Assert.assertEquals("t1", errors.get(0).getToken()); } + @Test + public void testHandleBulkDeleteTokensBatchException_errorFieldAsObjectUsesHelper() { + // Structured error envelope {"error": {message, httpCode}} → parsed per token via the helper. + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + errorObject.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getHttpCode()); + Assert.assertEquals("vault not found", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_bodyWithNeitherTokensNorErrorKey() { + // A map body matching neither branch falls through to the batch-wide fallback. + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 1, 50); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(Integer.valueOf(503), errors.get(0).getHttpCode()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + // startIndex = batchNumber * batchSize = 50 + Assert.assertEquals(50, errors.get(0).getIndex()); + Assert.assertEquals(51, errors.get(1).getIndex()); + Assert.assertEquals("t2", errors.get(1).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_tokensNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("tokens", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("tokens", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + // No entry parsed, so the batch-wide fallback fires instead. + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_recordEchoesValueAndReadsHttpCodeAndMessage() { + // createDeleteTokensErrorRecord: http_code key, "message" key, and an echoed "value" token. + Map tokenMap = new HashMap<>(); + tokenMap.put("http_code", 409); + tokenMap.put("message", "already deleted"); + tokenMap.put("value", "echoed-token"); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(tokenMap)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 409, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("requested-token")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(409), errors.get(0).getHttpCode()); + Assert.assertEquals("already deleted", errors.get(0).getError()); + // the echoed "value" wins over the token from the request batch + Assert.assertEquals("echoed-token", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_recordUsesStatusCodeAndUnknownError() { + // createDeleteTokensErrorRecord: statusCode key and the no-error/no-message fallback. + Map tokenMap = new HashMap<>(); + tokenMap.put("statusCode", 410); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(tokenMap)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 410, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(410), errors.get(0).getHttpCode()); + Assert.assertEquals("Unknown error", errors.get(0).getError()); + // no echoed value, so the requested token is reported + Assert.assertEquals("t1", errors.get(0).getToken()); + } + // ── handleBulkTokenizeBatchException ─────────────────────────────────────── private static List tokenizeBatch(String value, String... groups) { @@ -1416,6 +1612,75 @@ public void testHandleBulkTokenizeBatchException_noTokenGroupsStillReportsOneEnt Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError()); } + @Test + public void testHandleBulkTokenizeBatchException_errorBodyWithResponseArrayRebuildsRecords() { + // A 4xx whose body echoes the per-row "response" array is rebuilt via tokenizeRecordsFromErrorBody + // rather than summarized by the bare status code. + Map tokenRow = new HashMap<>(); + tokenRow.put("tokenGroupName", "group1"); + tokenRow.put("error", "BYOT token should contain one token group"); + tokenRow.put("httpCode", 400); + Map responseRow = new HashMap<>(); + responseRow.put("value", "v1"); + responseRow.put("tokens", Collections.singletonList(tokenRow)); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(responseRow)); + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("v1", errors.get(0).getValue()); + Assert.assertEquals(1, errors.get(0).getTokens().size()); + Assert.assertEquals("group1", errors.get(0).getTokens().get(0).getTokenGroupName()); + Assert.assertEquals("BYOT token should contain one token group", + errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_errorFieldAsObjectUsesStructuredMessage() { + // extractBatchErrorMessage reads {"error": {message}} when the body has no per-row response. + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("vault not found", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_nonMapBodyUsesApiMessage() { + // Body is not a map, so extractBatchErrorMessage falls back to the exception's own message. + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 500, "raw string body"); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("tokenize failed", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_nullBatchReturnsEmpty() { + RuntimeException ex = new RuntimeException("boom"); + + List errors = Utils.handleBulkTokenizeBatchException(ex, null, 0); + + Assert.assertTrue(errors.isEmpty()); + } + // ── formatBulkInsertResponse ─────────────────────────────────────────────── @Test diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index b37d809a..d9b7d30f 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -34,9 +34,9 @@ import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeOptions; +import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.DeleteTokensOptions; -import com.skyflow.vault.data.DetokenizeOptions; -import com.skyflow.vault.data.InsertOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestInterceptor; import com.skyflow.vault.data.TokenGroupRedactions; @@ -196,7 +196,7 @@ public void testBulkInsert_interceptorAddsCustomHeader() throws Exception { BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountId, "acct-123"); - InsertOptions options = InsertOptions.builder().interceptor(interceptor).build(); + BulkInsertOptions options = BulkInsertOptions.builder().interceptor(interceptor).build(); controller.bulkInsert(request, options); @@ -931,7 +931,7 @@ public void testBulkInsert_interceptorInvokedOncePerBatchWithDistinctContext() t .build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkInsert(request, InsertOptions.builder().interceptor(interceptor).build()); + controller.bulkInsert(request, BulkInsertOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(any(), captor.capture()); @@ -1000,7 +1000,7 @@ public void testBulkDetokenize_interceptorInvokedOncePerBatchWithDistinctContext BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(multiBatchTokens()).build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkDetokenize(request, DetokenizeOptions.builder().interceptor(interceptor).build()); + controller.bulkDetokenize(request, BulkDetokenizeOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(any(), captor.capture()); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java index f88c7a39..6f16c1dd 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java @@ -5,7 +5,8 @@ /** * Tests for the single-field options/builder classes: {@link InsertOptions}, - * {@link DetokenizeOptions}, {@link TokenizeOptions} and {@link DeleteTokensOptions}. + * {@link DetokenizeOptions}, {@link TokenizeOptions} and {@link DeleteTokensOptions}, + * plus the bulk specializations {@link BulkInsertOptions} and {@link BulkDetokenizeOptions}. * Each class simply wraps a {@link RequestInterceptor} with no validation. */ public class OptionsTests { @@ -41,6 +42,37 @@ public void testDetokenizeOptions_withoutInterceptor() { Assert.assertNull(options.getInterceptor()); } + // ── BulkInsertOptions ──────────────────────────────────────────────────── + + @Test + public void testBulkInsertOptions_withInterceptor() { + BulkInsertOptions options = BulkInsertOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + // BulkInsertOptions is a specialization of InsertOptions, so it flows anywhere the base does + Assert.assertTrue(options instanceof InsertOptions); + } + + @Test + public void testBulkInsertOptions_withoutInterceptor() { + BulkInsertOptions options = BulkInsertOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + // ── BulkDetokenizeOptions ──────────────────────────────────────────────── + + @Test + public void testBulkDetokenizeOptions_withInterceptor() { + BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + Assert.assertTrue(options instanceof DetokenizeOptions); + } + + @Test + public void testBulkDetokenizeOptions_withoutInterceptor() { + BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + // ── TokenizeOptions ────────────────────────────────────────────────────── @Test