Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/main/java/com/cyberark/conjur/api/Conjur.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cyberark.conjur.api;

import javax.net.ssl.SSLContext;
import java.util.Map;

/**
* Entry point for the Conjur API client.
Expand Down Expand Up @@ -105,4 +106,14 @@ public Conjur(Token token, SSLContext sslContext) {
public Variables variables() {
return variables;
}

/**
* Fetch multiple secret values in one invocation.
*
* @param variableIds the variable IDs to retrieve
* @return a map of variable ID to secret value
*/
public Map<String, String> retrieveBatchSecrets(String... variableIds) {
return variables.retrieveBatchSecrets(variableIds);
}
}
97 changes: 68 additions & 29 deletions src/main/java/com/cyberark/conjur/api/Endpoints.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,100 @@
import java.net.URI;

/**
* An <code>Endpoints</code> instance provides endpoint URIs for the various conjur services.
* An <code>Endpoints</code> instance provides endpoint URIs for the various Conjur services.
*
* <p>The canonical way to construct an {@code Endpoints} is from an appliance URL and account name.
* All service URIs are derived from these two values:</p>
* <ul>
* <li>Authentication: {@code {applianceUrl}/authn/{account}}</li>
* <li>Secrets (single): {@code {applianceUrl}/secrets/{account}/variable}</li>
* <li>Secrets (batch): {@code {applianceUrl}/secrets}</li>
* </ul>
*
* <p>For non-standard authenticators (LDAP, OIDC, etc.), supply a custom authn URL.</p>
*/
public class Endpoints implements Serializable {

private static final long serialVersionUID = 1L;
private final String applianceUrl;
private final String account;
private final URI authnUri;
private final URI secretsUri;

public Endpoints(final URI authnUri, final URI secretsUri){
this.authnUri = Args.notNull(authnUri, "authnUri");
this.secretsUri = Args.notNull(secretsUri, "secretsUri");
/**
* Create Endpoints from appliance URL and account, using standard authentication.
*
* @param applianceUrl the base Conjur appliance URL (e.g. {@code https://conjur.example.com})
* @param account the Conjur account name (e.g. {@code conjur} for SaaS, or your org name)
*/
public Endpoints(String applianceUrl, String account) {
this(applianceUrl, account, applianceUrl + "/authn");
}

public Endpoints(String authnUri, String secretsUri){
this(URI.create(authnUri), URI.create(secretsUri));
/**
* Create Endpoints from appliance URL, account, and a custom authentication URL.
* Use this when authenticating via a non-standard authenticator (LDAP, OIDC, etc.).
*
* @param applianceUrl the base Conjur appliance URL
* @param account the Conjur account name
* @param authnUrl the authentication service base URL
* (e.g. {@code https://conjur.example.com/authn-ldap/my-service})
*/
public Endpoints(String applianceUrl, String account, String authnUrl) {
this.applianceUrl = Args.notNull(applianceUrl, "applianceUrl");
this.account = Args.notNull(account, "account");
this.authnUri = URI.create(String.format("%s/%s", authnUrl, account));
this.secretsUri = URI.create(String.format("%s/secrets/%s/variable", applianceUrl, account));
}

public URI getAuthnUri(){ return authnUri; }
public URI getAuthnUri() { return authnUri; }

public URI getSecretsUri() {
return secretsUri;
public URI getSecretsUri() { return secretsUri; }

public String getAccount() { return account; }

public String getApplianceUrl() { return applianceUrl; }

/**
* Returns the base URI for batch secret retrieval: {@code {applianceUrl}/secrets}
*
* @return the batch secrets URI
*/
public URI getBatchSecretsUri() {
return URI.create(applianceUrl + "/secrets");
}

public static Endpoints fromSystemProperties(){
/**
* Create Endpoints from system properties / environment variables.
* Reads {@code CONJUR_ACCOUNT}, {@code CONJUR_APPLIANCE_URL}, and optionally {@code CONJUR_AUTHN_URL}.
*/
public static Endpoints fromSystemProperties() {
String account = Properties.getMandatoryProperty(Constants.CONJUR_ACCOUNT_PROPERTY);
String applianceUrl = Properties.getMandatoryProperty(Constants.CONJUR_APPLIANCE_URL_PROPERTY);
String authnUrl = Properties.getMandatoryProperty(Constants.CONJUR_AUTHN_URL_PROPERTY, applianceUrl + "/authn");
String authnUrl = Properties.getMandatoryProperty(
Constants.CONJUR_AUTHN_URL_PROPERTY, applianceUrl + "/authn");

return new Endpoints(
getAuthnServiceUri(authnUrl, account),
getServiceUri("secrets", account, "variable")
);
return new Endpoints(applianceUrl, account, authnUrl);
}

public static Endpoints fromCredentials(Credentials credentials){
/**
* Create Endpoints using the authentication URL from the given credentials.
* Account and appliance URL are read from system properties / environment variables.
*/
public static Endpoints fromCredentials(Credentials credentials) {
String account = Properties.getMandatoryProperty(Constants.CONJUR_ACCOUNT_PROPERTY);
return new Endpoints(
getAuthnServiceUri(credentials.getAuthnUrl(), account),
getServiceUri("secrets", account, "variable")
);
}

private static URI getAuthnServiceUri(String authnUrl, String accountName) {
return URI.create(String.format("%s/%s", authnUrl, accountName));
}
String applianceUrl = Properties.getMandatoryProperty(Constants.CONJUR_APPLIANCE_URL_PROPERTY);

private static URI getServiceUri(String service, String accountName, String path){
return URI.create(String.format("%s/%s/%s/%s", Properties.getMandatoryProperty(Constants.CONJUR_APPLIANCE_URL_PROPERTY), service, accountName, path));
return new Endpoints(applianceUrl, account, credentials.getAuthnUrl());
}

@Override
public String toString() {
return "Endpoints{" +
"authnUri=" + authnUri +
"secretsUri=" + secretsUri +
"applianceUrl=" + applianceUrl +
", account=" + account +
", authnUri=" + authnUri +
", secretsUri=" + secretsUri +
'}';
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/cyberark/conjur/api/ResourceProvider.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.cyberark.conjur.api;

import java.util.Map;

/**
* Provides methods for retrieving and setting Conjur resources
*/
Expand All @@ -19,4 +21,16 @@ public interface ResourceProvider {
*/
void addSecret(String variableId, String secret);

/**
* Fetch multiple secret values in one invocation.
* It's faster to fetch secrets in batches than to fetch them one at a time.
*
* @param variableIds the variable IDs to retrieve (without account prefix)
* @return a map of variable ID to secret value
* @see <a href="https://docs.cyberark.com/conjur-open-source/latest/en/content/developer/conjur_api_batch_retrieve.htm">Batch Secret Retrieval</a>
*/
default Map<String, String> retrieveBatchSecrets(String... variableIds) {
throw new UnsupportedOperationException("Batch secret retrieval not supported");
}

}
11 changes: 11 additions & 0 deletions src/main/java/com/cyberark/conjur/api/Variables.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cyberark.conjur.api;

import javax.net.ssl.SSLContext;
import java.util.Map;

import com.cyberark.conjur.api.clients.ResourceClient;

Expand Down Expand Up @@ -32,4 +33,14 @@ public String retrieveSecret(String variableId) {
public void addSecret(String variableId, String secret){
resourceClient.addSecret(variableId, secret);
}

/**
* Fetch multiple secret values in one invocation.
*
* @param variableIds the variable IDs to retrieve
* @return a map of variable ID to secret value
*/
public Map<String, String> retrieveBatchSecrets(String... variableIds) {
return resourceClient.retrieveBatchSecrets(variableIds);
}
}
92 changes: 90 additions & 2 deletions src/main/java/com/cyberark/conjur/api/clients/ResourceClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;

import java.lang.reflect.Type;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import com.cyberark.conjur.api.Configuration;
import com.cyberark.conjur.api.Credentials;
import com.cyberark.conjur.api.Endpoints;
Expand All @@ -21,6 +29,11 @@
*/
public class ResourceClient implements ResourceProvider {

private static final Type MAP_STRING_STRING_TYPE =
new TypeToken<Map<String, String>>(){}.getType();
private static final Gson GSON = new Gson();

private Client client;
private WebTarget secrets;
private final Endpoints endpoints;

Expand Down Expand Up @@ -50,6 +63,13 @@ public ResourceClient(final Token token,
init(token, sslContext);
}

// Package-private constructor for unit testing with mock clients
ResourceClient(Client client, WebTarget secrets, Endpoints endpoints) {
this.client = client;
this.secrets = secrets;
this.endpoints = endpoints;
}

@Override
public String retrieveSecret(String variableId) {
Response response = secrets.path(encodeVariableId(variableId))
Expand All @@ -66,6 +86,74 @@ public void addSecret(String variableId, String secret) {
validateResponse(response);
}

/**
* Fetch multiple secret values in one invocation using the batch retrieval API.
* <p>
* Constructs fully-qualified variable IDs ({account}:variable:{id}) and sends them
* as a comma-delimited list in the {@code variable_ids} query parameter.
* </p>
*
* @param variableIds the variable IDs to retrieve (without account/kind prefix)
* @return a map of variable ID (as passed by caller) to secret value
* @throws IllegalArgumentException if no variable IDs are provided or account is not configured
* @throws WebApplicationException if the server returns an error response
* @see <a href="https://docs.cyberark.com/conjur-open-source/latest/en/content/developer/conjur_api_batch_retrieve.htm">Batch Secret Retrieval</a>
*/
@Override
public Map<String, String> retrieveBatchSecrets(String... variableIds) {
if (variableIds == null || variableIds.length == 0) {
throw new IllegalArgumentException("At least one variable ID must be provided");
}

String account = endpoints.getAccount();
if (account == null || account.isEmpty()) {
throw new IllegalArgumentException("Account is not configured in Endpoints");
}

// Build the comma-delimited fully-qualified variable IDs for the query parameter.
// Format: {account}:variable:{encoded_id1},{account}:variable:{encoded_id2}
// Colons and commas are valid in URI query components (RFC 3986) and must NOT be encoded.
// Only the variable ID portion is percent-encoded.
String queryValue = buildBatchQueryParam(account, variableIds);

// Build the full URI manually to avoid double-encoding by JAX-RS queryParam()
URI batchUri = URI.create(endpoints.getBatchSecretsUri().toString()
+ "?variable_ids=" + queryValue);

Response response = client.target(batchUri).request().get(Response.class);
validateResponse(response);

String json = response.readEntity(String.class);
Map<String, String> raw = GSON.fromJson(json, MAP_STRING_STRING_TYPE);

// Map fully-qualified IDs back to the caller's variable IDs
String prefix = account + ":variable:";
Map<String, String> result = new LinkedHashMap<String, String>();
for (Map.Entry<String, String> entry : raw.entrySet()) {
String key = entry.getKey();
if (key.startsWith(prefix)) {
result.put(key.substring(prefix.length()), entry.getValue());
} else {
result.put(key, entry.getValue());
}
}
return result;
}

/**
* Build the comma-separated query parameter value for batch retrieval.
* Visible for testing.
*/
String buildBatchQueryParam(String account, String... variableIds) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < variableIds.length; i++) {
if (i > 0) sb.append(",");
sb.append(account).append(":variable:")
.append(encodeVariableId(variableIds[i]));
}
return sb.toString();
}

// The "encodeUriComponent" method encodes plus signs into %2B and spaces
// into '+'. However, our server decodes plus signs into plus signs in the
// retrieveSecret request so we need to replace the plus signs (which are
Expand All @@ -89,7 +177,7 @@ private void init(Credentials credentials, SSLContext sslContext){
builder.sslContext(sslContext);
}

Client client = builder.build();
this.client = builder.build();

secrets = client.target(getEndpoints().getSecretsUri());
}
Expand All @@ -105,7 +193,7 @@ private void init(Token token, SSLContext sslContext){
builder.sslContext(sslContext);
}

Client client = builder.build();
this.client = builder.build();

secrets = client.target(getEndpoints().getSecretsUri());
}
Expand Down
Loading