From c6830246d893bd2c4850660569eae4b849b12822 Mon Sep 17 00:00:00 2001 From: Sam Sternberg Date: Tue, 7 Apr 2026 08:45:31 -0400 Subject: [PATCH] feat(auth): Add JSON object context support for Conditional Data Access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ctx claim in bearer tokens and signed data tokens previously only accepted a String, which meant structured CEL expressions like request.context.role == 'admin' could not be satisfied. Add setCtx(Map) overloads to BearerToken and SignedDataTokens builders so the JWT ctx claim is serialized as a nested JSON object. Also add setContext(Map) and getContextAsObject() to Credentials for use with the high-level Skyflow client. All changes are backwards compatible — existing setCtx(String) and setContext(String) APIs are unchanged. Refs SK-2679 Co-Authored-By: Claude --- README.md | 101 +++++++++--------- ...arerTokenGenerationWithContextExample.java | 37 ++++++- .../SignedTokenGenerationExample.java | 40 ++++++- .../java/com/skyflow/config/Credentials.java | 11 +- .../java/com/skyflow/errors/ErrorMessage.java | 2 + src/main/java/com/skyflow/logs/ErrorLogs.java | 2 + .../serviceaccount/util/BearerToken.java | 38 +++++-- .../serviceaccount/util/SignedDataTokens.java | 38 +++++-- src/main/java/com/skyflow/utils/Utils.java | 29 +++-- .../utils/validations/Validations.java | 42 +++++++- .../com/skyflow/config/CredentialsTests.java | 91 ++++++++++++++++ .../serviceaccount/util/BearerTokenTests.java | 90 ++++++++++++++++ .../util/SignedDataTokensTests.java | 96 +++++++++++++++++ 13 files changed, 527 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 023bb288..0f37e1a7 100644 --- a/README.md +++ b/README.md @@ -2771,10 +2771,14 @@ public class BearerTokenGenerationExample { ## Generate bearer tokens with context -**Context-aware authorization** embeds context values into a bearer token during its generation and so you can reference those values in your policies. This enables more flexible access controls, such as helping you track end-user identity when making API calls using service accounts, and facilitates using signed data tokens during detokenization. . +**Context-aware authorization** embeds context values into a bearer token during its generation and so you can reference those values in your policies. This enables more flexible access controls, such as helping you track end-user identity when making API calls using service accounts, and facilitates using signed data tokens during detokenization. A service account with the `context_id` identifier generates bearer tokens containing context information, represented as a JWT claim in a Skyflow-generated bearer token. Tokens generated from such service accounts include a `context_identifier` claim, are valid for 60 minutes, and can be used to make API calls to the Data and Management APIs, depending on the service account's permissions. +The context can be provided as a simple string or as a `Map` for structured context. Use a `Map` when your policies use Conditional Data Access with CEL expressions that reference nested context fields (e.g., `request.context.role == 'admin'`). + +### String context + [Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java): ```java @@ -2783,60 +2787,44 @@ import com.skyflow.serviceaccount.util.BearerToken; import java.io.File; -/** - * Example program to generate a Bearer Token using Skyflow's BearerToken utility. - * The token is generated using two approaches: - * 1. By providing the credentials.json file path. - * 2. By providing the contents of credentials.json as a string. - */ -public class BearerTokenGenerationWithContextExample { - public static void main(String[] args) { - // Variable to store the generated Bearer Token - String bearerToken = null; +// Generate Bearer Token with a simple string context +String filePath = ""; - // Approach 1: Generate Bearer Token by specifying the path to the credentials.json file - try { - // Replace with the full path to your credentials.json file - String filePath = ""; +BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx("abc") // Simple string context + .build(); - // Create a BearerToken object using the file path - BearerToken token = BearerToken.builder() - .setCredentials(new File(filePath)) // Set credentials using a File object - .setCtx("abc") // Set context string (example: "abc") - .build(); // Build the BearerToken object +String bearerToken = token.getBearerToken(); +``` - // Retrieve the Bearer Token as a string - bearerToken = token.getBearerToken(); +### JSON object context (Conditional Data Access) - // Print the generated Bearer Token to the console - System.out.println(bearerToken); - } catch (SkyflowException e) { - // Handle exceptions specific to Skyflow operations - e.printStackTrace(); - } +Skyflow's [Conditional Data Access](https://docs.skyflow.com/docs/governance/roles/conditional-data-access/overview) feature enables dynamic, context-aware access control by allowing roles to activate only when specific conditions are met at runtime. Conditions are defined using Common Expression Language (CEL) expressions that evaluate against `request.context`, `request.time`, and `request.originIP`. - // Approach 2: Generate Bearer Token by specifying the contents of credentials.json as a string - try { - // Replace with the actual contents of your credentials.json file - String fileContents = ""; +To satisfy context-based conditions, pass a `Map` to `setCtx()`. The map is embedded as a nested JSON object in the JWT `ctx` claim, allowing CEL expressions to reference individual fields. - // Create a BearerToken object using the file contents as a string - BearerToken token = BearerToken.builder() - .setCredentials(fileContents) // Set credentials using a string representation of the file - .setCtx("abc") // Set context string (example: "abc") - .build(); // Build the BearerToken object +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; - // Retrieve the Bearer Token as a string - bearerToken = token.getBearerToken(); +import java.io.File; +import java.util.HashMap; +import java.util.Map; - // Print the generated Bearer Token to the console - System.out.println(bearerToken); - } catch (SkyflowException e) { - // Handle exceptions specific to Skyflow operations - e.printStackTrace(); - } - } -} +// Create a context map matching your Conditional Data Access policy +// For example, if your policy condition is: +// request.context.role == 'admin' && request.context.project_id == 'proj_123' +Map context = new HashMap<>(); +context.put("role", "admin"); +context.put("project_id", "proj_123"); + +BearerToken token = BearerToken.builder() + .setCredentials(new File("")) + .setCtx(context) // JSON object context for Conditional Data Access + .build(); + +String bearerToken = token.getBearerToken(); ``` ## Generate scoped bearer tokens @@ -2903,6 +2891,8 @@ with the private key of the service account credentials, which adds an additiona be detokenized by passing the signed data token and a bearer token generated from service account credentials. The service account must have appropriate permissions and context to detokenize the signed data tokens. +Like bearer tokens, the context can be provided as a simple string or as a `Map` for Conditional Data Access. + [Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java): ```java @@ -2912,12 +2902,15 @@ 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; public class SignedTokenGenerationExample { public static void main(String[] args) { List signedTokenValues; - // Generate Signed data token with context by specifying credentials.json file path + + // Generate Signed data token with string context try { String filePath = ""; String context = "abc"; @@ -2935,15 +2928,17 @@ public class SignedTokenGenerationExample { e.printStackTrace(); } - // Generate Signed data token with context by specifying credentials.json as string + // Generate Signed data token with JSON object context for Conditional Data Access try { - String fileContents = ""; - String context = "abc"; + String filePath = ""; + Map context = new HashMap<>(); + context.put("role", "admin"); + context.put("project_id", "proj_123"); ArrayList dataTokens = new ArrayList<>(); dataTokens.add("YOUR_DATA_TOKEN_1"); SignedDataTokens signedToken = SignedDataTokens.builder() - .setCredentials(fileContents) - .setCtx(context) + .setCredentials(new File(filePath)) + .setCtx(context) // JSON object context .setTimeToLive(30) // in seconds .setDataTokens(dataTokens) .build(); diff --git a/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java b/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java index 3ed9e267..efd7f657 100644 --- a/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java +++ b/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java @@ -4,12 +4,15 @@ 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 two approaches: - * 1. By providing the credentials.json file path. - * 2. By providing the contents of credentials.json as a string. + * The token is generated using three approaches: + * 1. By providing the credentials.json file path with a string context. + * 2. By providing the contents of credentials.json as a string with a string context. + * 3. By providing a JSON object context for Conditional Data Access. */ public class BearerTokenGenerationWithContextExample { public static void main(String[] args) { @@ -57,5 +60,33 @@ public static void main(String[] args) { // Handle exceptions specific to Skyflow operations e.printStackTrace(); } + + // Approach 3: Generate Bearer Token with a JSON object context for Conditional Data Access + // Use this approach when your Skyflow policy uses CEL expressions that reference nested + // context fields, such as: request.context.role == 'admin' + try { + // Replace with the full path to your credentials.json file + String filePath = ""; + + // Create a context map with key-value pairs matching your Conditional Data Access policy + Map context = new HashMap<>(); + context.put("role", "admin"); // Evaluated as request.context.role + context.put("project_id", "proj_123"); // Evaluated as request.context.project_id + + // Create a BearerToken object with the JSON object context + BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) // Set credentials using a File object + .setCtx(context) // Set context as a JSON object + .build(); // Build the BearerToken object + + // Retrieve the Bearer Token as a string + bearerToken = token.getBearerToken(); + + // Print the generated Bearer Token to the console + System.out.println(bearerToken); + } catch (SkyflowException e) { + // Handle exceptions specific to Skyflow operations + e.printStackTrace(); + } } } diff --git a/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java b/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java index 98f552f3..d2789c65 100644 --- a/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java +++ b/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java @@ -6,12 +6,15 @@ 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 two methods: - * 1. Specifying the path to a credentials JSON file. - * 2. Providing the credentials JSON as a string. + * This example demonstrates how to generate Signed Data Tokens using three methods: + * 1. Specifying the path to a credentials JSON file with a string context. + * 2. Providing the credentials JSON as a string with a string context. + * 3. Using a JSON object context for Conditional Data Access. *

* Signed data tokens are used to verify and securely transmit data with a specified context and TTL. */ @@ -70,5 +73,36 @@ public static void main(String[] args) { System.out.println("Error occurred while generating signed tokens using credentials string:"); e.printStackTrace(); } + + // Example 3: Generate Signed Data Token with a JSON object context for Conditional Data Access + // Use this approach when your Skyflow policy uses CEL expressions that reference nested + // context fields, such as: request.context.role == 'admin' + try { + // Step 1: Specify the path to the service account credentials JSON file + String filePath = ""; // Replace with the actual file path + + // Step 2: Create a context map with key-value pairs matching your Conditional Data Access policy + Map context = new HashMap<>(); + context.put("role", "admin"); // Evaluated as request.context.role + context.put("project_id", "proj_123"); // Evaluated as request.context.project_id + + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); // Replace with your actual data token(s) + + // Step 3: Build the SignedDataTokens object with the JSON object context + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) // Provide the credentials file + .setCtx(context) // Set context as a JSON object + .setTimeToLive(30) // Set the TTL (in seconds) + .setDataTokens(dataTokens) // Set the data tokens to sign + .build(); + + // Step 4: Retrieve and print the signed data tokens + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (using JSON object context): " + signedTokenValues); + } catch (SkyflowException e) { + System.out.println("Error occurred while generating signed tokens with JSON object context:"); + e.printStackTrace(); + } } } diff --git a/src/main/java/com/skyflow/config/Credentials.java b/src/main/java/com/skyflow/config/Credentials.java index f1865dc7..e70dd124 100644 --- a/src/main/java/com/skyflow/config/Credentials.java +++ b/src/main/java/com/skyflow/config/Credentials.java @@ -1,11 +1,12 @@ package com.skyflow.config; import java.util.ArrayList; +import java.util.Map; public class Credentials { private String path; private ArrayList roles; - private String context; + private Object context; private String credentialsString; private String token; private String apiKey; @@ -33,6 +34,10 @@ public void setRoles(ArrayList roles) { } public String getContext() { + return context instanceof String ? (String) context : null; + } + + public Object getContextAsObject() { return context; } @@ -40,6 +45,10 @@ public void setContext(String context) { this.context = context; } + public void setContext(Map context) { + this.context = context; + } + public String getCredentialsString() { return credentialsString; } diff --git a/src/main/java/com/skyflow/errors/ErrorMessage.java b/src/main/java/com/skyflow/errors/ErrorMessage.java index 8885a6c8..63e992af 100644 --- a/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -34,6 +34,8 @@ public enum ErrorMessage { EmptyRoles("%s0 Initialization failed. Invalid roles. Specify at least one role."), EmptyRoleInRoles("%s0 Initialization failed. Invalid role. Specify a valid role."), EmptyContext("%s0 Initialization failed. Invalid context. Specify a valid context."), + InvalidCtxType("%s0 Initialization failed. Invalid context type. Context must be a string or a map."), + InvalidCtxMapKey("%s0 Initialization failed. Invalid context map key '%s1'. Context map keys must contain only alphanumeric characters and underscores."), // Bearer token generation FileNotFound("%s0 Initialization failed. Credential file not found at %s1. Verify the file path."), diff --git a/src/main/java/com/skyflow/logs/ErrorLogs.java b/src/main/java/com/skyflow/logs/ErrorLogs.java index eb5ea742..42df67fc 100644 --- a/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -25,6 +25,8 @@ public enum ErrorLogs { EMPTY_ROLES("Invalid credentials. Roles can not be empty."), EMPTY_OR_NULL_ROLE_IN_ROLES("Invalid credentials. Role can not be null or empty in roles at index %s1."), EMPTY_OR_NULL_CONTEXT("Invalid credentials. Context can not be empty."), + INVALID_CTX_TYPE("Invalid credentials. Context must be a string or a map."), + INVALID_CTX_MAP_KEY("Invalid credentials. Context map key '%s1' is invalid. Keys must match ^[a-zA-Z0-9_]+$."), // Bearer token generation INVALID_BEARER_TOKEN("Bearer token is invalid or expired."), diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java index 21000f0f..92c617ab 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +++ b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java @@ -17,6 +17,8 @@ import com.skyflow.utils.logger.LogUtil; import io.jsonwebtoken.Jwts; +import com.skyflow.utils.validations.Validations; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -24,6 +26,7 @@ import java.security.PrivateKey; import java.util.ArrayList; import java.util.Date; +import java.util.Map; import java.util.Objects; public class BearerToken { @@ -31,7 +34,7 @@ public class BearerToken { private static final ApiClientBuilder API_CLIENT_BUILDER = new ApiClientBuilder(); private final File credentialsFile; private final String credentialsString; - private final String ctx; + private final Object ctx; private final ArrayList roles; private final String credentialsType; @@ -48,7 +51,7 @@ public static BearerTokenBuilder builder() { } private static V1GetAuthTokenResponse generateBearerTokenFromCredentials( - File credentialsFile, String context, ArrayList roles + File credentialsFile, Object context, ArrayList roles ) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_TRIGGERED.getLog()); try { @@ -71,7 +74,7 @@ private static V1GetAuthTokenResponse generateBearerTokenFromCredentials( } private static V1GetAuthTokenResponse generateBearerTokenFromCredentialString( - String credentials, String context, ArrayList roles + String credentials, Object context, ArrayList roles ) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_STRING_TRIGGERED.getLog()); try { @@ -89,7 +92,7 @@ private static V1GetAuthTokenResponse generateBearerTokenFromCredentialString( } private static V1GetAuthTokenResponse getBearerTokenFromCredentials( - JsonObject credentials, String context, ArrayList roles + JsonObject credentials, Object context, ArrayList roles ) throws SkyflowException { try { JsonElement privateKey = credentials.get("privateKey"); @@ -144,8 +147,19 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( } private static String getSignedToken( - String clientID, String keyID, String tokenURI, PrivateKey pvtKey, String context - ) { + String clientID, String keyID, String tokenURI, PrivateKey pvtKey, Object context + ) throws SkyflowException { + // Validate and normalize context + Object validatedContext = context; + if (context instanceof Map) { + Map ctxMap = (Map) context; + if (ctxMap.isEmpty()) { + validatedContext = null; + } else { + Validations.validateCtxMapKeys(ctxMap); + } + } + final Date createdDate = new Date(); final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000)); return Jwts.builder() @@ -153,7 +167,7 @@ private static String getSignedToken( .claim("key", keyID) .claim("aud", tokenURI) .claim("sub", clientID) - .claim("ctx", context) + .claim("ctx", validatedContext) .expiration(expirationDate) .signWith(pvtKey, Jwts.SIG.RS256) .compact(); @@ -188,7 +202,7 @@ public synchronized String getBearerToken() throws SkyflowException { public static class BearerTokenBuilder { private File credentialsFile; private String credentialsString; - private String ctx; + private Object ctx; private ArrayList roles; private String credentialsType; @@ -216,6 +230,14 @@ public BearerTokenBuilder setCtx(String ctx) { return this; } + public BearerTokenBuilder setCtx(Map ctx) throws SkyflowException { + if (ctx != null && !ctx.isEmpty()) { + Validations.validateCtxMapKeys(ctx); + } + this.ctx = ctx; + return this; + } + public BearerTokenBuilder setRoles(ArrayList roles) { this.roles = roles; return this; diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java index 70a5a330..394835e0 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +++ b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java @@ -13,6 +13,8 @@ import com.skyflow.utils.logger.LogUtil; import io.jsonwebtoken.Jwts; +import com.skyflow.utils.validations.Validations; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -20,13 +22,14 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; public class SignedDataTokens { private final File credentialsFile; private final String credentialsString; private final String credentialsType; - private final String ctx; + private final Object ctx; private final ArrayList dataTokens; private final Integer timeToLive; @@ -44,7 +47,7 @@ public static SignedDataTokensBuilder builder() { } private static List generateSignedTokenFromCredentialsFile( - File credentialsFile, ArrayList dataTokens, Integer timeToLive, String context + File credentialsFile, ArrayList dataTokens, Integer timeToLive, Object context ) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.GENERATE_SIGNED_TOKENS_FROM_CREDENTIALS_FILE_TRIGGERED.getLog()); List responseToken; @@ -69,7 +72,7 @@ private static List generateSignedTokenFromCredentialsF } private static List generateSignedTokensFromCredentialsString( - String credentials, ArrayList dataTokens, Integer timeToLive, String context + String credentials, ArrayList dataTokens, Integer timeToLive, Object context ) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.GENERATE_SIGNED_TOKENS_FROM_CREDENTIALS_STRING_TRIGGERED.getLog()); List responseToken; @@ -89,7 +92,7 @@ private static List generateSignedTokensFromCredentials } private static List generateSignedTokensFromCredentials( - JsonObject credentials, ArrayList dataTokens, Integer timeToLive, String context + JsonObject credentials, ArrayList dataTokens, Integer timeToLive, Object context ) throws SkyflowException { List signedDataTokens = null; try { @@ -122,8 +125,19 @@ private static List generateSignedTokensFromCredentials private static List getSignedToken( String clientID, String keyID, PrivateKey pvtKey, - ArrayList dataTokens, Integer timeToLive, String context - ) { + ArrayList dataTokens, Integer timeToLive, Object context + ) throws SkyflowException { + // Validate and normalize context + Object validatedContext = context; + if (context instanceof Map) { + Map ctxMap = (Map) context; + if (ctxMap.isEmpty()) { + validatedContext = null; + } else { + Validations.validateCtxMapKeys(ctxMap); + } + } + final Date createdDate = new Date(); final Date expirationDate; @@ -140,7 +154,7 @@ private static List getSignedToken( .claim("iat", (createdDate.getTime() / 1000)) .claim("key", keyID) .claim("sub", clientID) - .claim("ctx", context) + .claim("ctx", validatedContext) .claim("tok", dataToken) .expiration(expirationDate) .signWith(pvtKey, Jwts.SIG.RS256) @@ -168,7 +182,7 @@ public static class SignedDataTokensBuilder { private Integer timeToLive; private File credentialsFile; private String credentialsString; - private String ctx; + private Object ctx; private String credentialsType; private SignedDataTokensBuilder() { @@ -195,6 +209,14 @@ public SignedDataTokensBuilder setCtx(String ctx) { return this; } + public SignedDataTokensBuilder setCtx(Map ctx) throws SkyflowException { + if (ctx != null && !ctx.isEmpty()) { + Validations.validateCtxMapKeys(ctx); + } + this.ctx = ctx; + return this; + } + public SignedDataTokensBuilder setDataTokens(ArrayList dataTokens) { this.dataTokens = dataTokens; return this; diff --git a/src/main/java/com/skyflow/utils/Utils.java b/src/main/java/com/skyflow/utils/Utils.java index 165c6a80..eec2d024 100644 --- a/src/main/java/com/skyflow/utils/Utils.java +++ b/src/main/java/com/skyflow/utils/Utils.java @@ -47,21 +47,30 @@ public static String getVaultURL(String clusterId, Env env) { return sb.toString(); } + @SuppressWarnings("unchecked") public static String generateBearerToken(Credentials credentials) throws SkyflowException { if (credentials.getPath() != null) { - return BearerToken.builder() + BearerToken.BearerTokenBuilder builder = BearerToken.builder() .setCredentials(new File(credentials.getPath())) - .setRoles(credentials.getRoles()) - .setCtx(credentials.getContext()) - .build() - .getBearerToken(); + .setRoles(credentials.getRoles()); + Object ctx = credentials.getContextAsObject(); + if (ctx instanceof Map) { + builder.setCtx((Map) ctx); + } else { + builder.setCtx((String) ctx); + } + return builder.build().getBearerToken(); } else if (credentials.getCredentialsString() != null) { - return BearerToken.builder() + BearerToken.BearerTokenBuilder builder = BearerToken.builder() .setCredentials(credentials.getCredentialsString()) - .setRoles(credentials.getRoles()) - .setCtx(credentials.getContext()) - .build() - .getBearerToken(); + .setRoles(credentials.getRoles()); + Object ctx = credentials.getContextAsObject(); + if (ctx instanceof Map) { + builder.setCtx((Map) ctx); + } else { + builder.setCtx((String) ctx); + } + return builder.build().getBearerToken(); } else { return credentials.getToken(); } diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java index 1d078fa9..908c3a14 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/src/main/java/com/skyflow/utils/validations/Validations.java @@ -42,9 +42,36 @@ import com.skyflow.vault.tokens.TokenizeRequest; public class Validations { + private static final Pattern CTX_MAP_KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$"); + private Validations() { } + public static void validateCtxMapKeys(Map ctxMap) throws SkyflowException { + for (Object key : ctxMap.keySet()) { + if (key == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_CTX_MAP_KEY.getLog(), "null" + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "null")); + } + String keyStr = key.toString(); + if (!CTX_MAP_KEY_PATTERN.matcher(keyStr).matches()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_CTX_MAP_KEY.getLog(), keyStr + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), keyStr)); + } + // Recursively validate nested maps + Object value = ctxMap.get(key); + if (value instanceof Map) { + validateCtxMapKeys((Map) value); + } + } + } + public static void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { String vaultId = vaultConfig.getVaultId(); String clusterId = vaultConfig.getClusterId(); @@ -162,7 +189,7 @@ public static void validateCredentials(Credentials credentials) throws SkyflowEx String credentialsString = credentials.getCredentialsString(); String token = credentials.getToken(); String apiKey = credentials.getApiKey(); - String context = credentials.getContext(); + Object context = credentials.getContextAsObject(); ArrayList roles = credentials.getRoles(); if (path != null) nonNullMembers++; @@ -217,9 +244,16 @@ public static void validateCredentials(Credentials credentials) throws SkyflowEx } } } - if (context != null && context.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + if (context != null) { + if (context instanceof String && ((String) context).trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } else if (context instanceof Map && ((Map) context).isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } else if (context instanceof Map) { + validateCtxMapKeys((Map) context); + } } } diff --git a/src/test/java/com/skyflow/config/CredentialsTests.java b/src/test/java/com/skyflow/config/CredentialsTests.java index ff9fcfda..6b3381d6 100644 --- a/src/test/java/com/skyflow/config/CredentialsTests.java +++ b/src/test/java/com/skyflow/config/CredentialsTests.java @@ -3,6 +3,7 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.Utils; import com.skyflow.utils.validations.Validations; import org.junit.Assert; import org.junit.Before; @@ -10,6 +11,8 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class CredentialsTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; @@ -266,4 +269,92 @@ public void testEmptyContextInCredentials() { } } + @Test + public void testValidCredentialsWithMapContext() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("role", "admin"); + mapContext.put("department", "engineering"); + Credentials credentials = new Credentials(); + credentials.setApiKey(validApiKey); + credentials.setContext(mapContext); + Validations.validateCredentials(credentials); + Assert.assertNull(credentials.getContext()); + Assert.assertEquals(mapContext, credentials.getContextAsObject()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testInvalidCtxMapKeyWithHyphenInCredentials() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid-key", "value"); + Credentials credentials = new Credentials(); + credentials.setPath(path); + credentials.setContext(mapContext); + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid-key"), + e.getMessage() + ); + } + } + + @Test + public void testInvalidCtxMapKeyWithDotInCredentials() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid.key", "value"); + Credentials credentials = new Credentials(); + credentials.setPath(path); + credentials.setContext(mapContext); + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid.key"), + e.getMessage() + ); + } + } + + @Test + public void testInvalidCtxMapKeyWithSpaceInCredentials() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid key", "value"); + Credentials credentials = new Credentials(); + credentials.setPath(path); + credentials.setContext(mapContext); + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid key"), + e.getMessage() + ); + } + } + + @Test + public void testEmptyMapContextInCredentials() { + try { + Credentials credentials = new Credentials(); + credentials.setPath(path); + credentials.setContext(new HashMap<>()); + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.EmptyContext.getMessage(), e.getMessage()); + } + } + } diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java index 4bfb697c..83d3f602 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java @@ -10,6 +10,8 @@ import java.io.File; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class BearerTokenTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; @@ -196,6 +198,94 @@ public void testInvalidPrivateKeyInCredentialsForCredentials() { } } + @Test + public void testBearerTokenBuilderWithMapContext() { + try { + File file = new File(credentialsFilePath); + Map mapContext = new HashMap<>(); + mapContext.put("role", "admin"); + mapContext.put("department", "engineering"); + BearerToken.builder().setCredentials(file).setCtx(mapContext).setRoles(roles).build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testBearerTokenBuilderWithNestedMapContext() { + try { + File file = new File(credentialsFilePath); + Map nestedContext = new HashMap<>(); + Map user = new HashMap<>(); + user.put("role", "admin"); + user.put("level", 5); + nestedContext.put("user", user); + nestedContext.put("project_id", "proj_123"); + BearerToken.builder().setCredentials(file).setCtx(nestedContext).build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testBearerTokenBuilderWithMapContextAndCredentialsString() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("role", "admin"); + BearerToken.builder().setCredentials(credentialsString).setCtx(mapContext).build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testBearerTokenWithInvalidCtxMapKeyContainingHyphen() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid-key", "value"); + BearerToken.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid-key"), + e.getMessage() + ); + } + } + + @Test + public void testBearerTokenWithInvalidCtxMapKeyContainingDot() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid.key", "value"); + BearerToken.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid.key"), + e.getMessage() + ); + } + } + + @Test + public void testBearerTokenWithInvalidCtxMapKeyContainingSpace() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid key", "value"); + BearerToken.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid key"), + e.getMessage() + ); + } + } + @Test public void testInvalidKeySpecInCredentialsForCredentials() { String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", \"clientID\": \"client_id_value\", \"keyID\": \"key_id_value\", \"tokenURI\": \"invalid_token_uri\"}"; diff --git a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java index f431042d..620b9fa0 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java @@ -10,6 +10,8 @@ import java.io.File; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class SignedDataTokensTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; @@ -60,6 +62,100 @@ public void testSignedDataTokensBuilderWithCredentialsString() { } + @Test + public void testSignedDataTokensBuilderWithMapContext() { + try { + File file = new File(credentialsFilePath); + Map mapContext = new HashMap<>(); + mapContext.put("role", "admin"); + mapContext.put("department", "engineering"); + SignedDataTokens.builder() + .setCredentials(file).setCtx(mapContext).setDataTokens(dataTokens).setTimeToLive(ttl) + .build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testSignedDataTokensBuilderWithNestedMapContext() { + try { + File file = new File(credentialsFilePath); + Map nestedContext = new HashMap<>(); + Map user = new HashMap<>(); + user.put("role", "admin"); + user.put("level", 5); + nestedContext.put("user", user); + nestedContext.put("project_id", "proj_123"); + SignedDataTokens.builder() + .setCredentials(file).setCtx(nestedContext).setDataTokens(dataTokens).setTimeToLive(ttl) + .build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testSignedDataTokensBuilderWithMapContextAndCredentialsString() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("role", "admin"); + SignedDataTokens.builder() + .setCredentials(credentialsString).setCtx(mapContext).setDataTokens(dataTokens).setTimeToLive(ttl) + .build(); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testSignedDataTokensWithInvalidCtxMapKeyContainingHyphen() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid-key", "value"); + SignedDataTokens.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid-key"), + e.getMessage() + ); + } + } + + @Test + public void testSignedDataTokensWithInvalidCtxMapKeyContainingDot() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid.key", "value"); + SignedDataTokens.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid.key"), + e.getMessage() + ); + } + } + + @Test + public void testSignedDataTokensWithInvalidCtxMapKeyContainingSpace() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("invalid key", "value"); + SignedDataTokens.builder().setCtx(mapContext); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + Utils.parameterizedString(ErrorMessage.InvalidCtxMapKey.getMessage(), "invalid key"), + e.getMessage() + ); + } + } + @Test public void testEmptyCredentialsFilePath() { try {