From de5a3368235c25a0a47e569f47b43bf92eccd3d0 Mon Sep 17 00:00:00 2001 From: Brad Nicholes Date: Mon, 10 Jul 2023 17:51:07 -0600 Subject: [PATCH] Add the APIs to set credentials using the A2A context. Update the tests to verify the new APIs. --- .../safeguardjava/ISafeguardA2AContext.java | 23 ++++- .../safeguardjava/SafeguardA2AContext.java | 88 ++++++++++++++++++- .../safeguard/safeguardjava/data/SshKey.java | 35 ++++++++ .../safeguardjava/restclient/RestClient.java | 19 ++++ .../safeguardclient/SafeguardTests.java | 77 +++++++++++++++- 5 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/oneidentity/safeguard/safeguardjava/data/SshKey.java diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/ISafeguardA2AContext.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/ISafeguardA2AContext.java index 574d3a2..c95e1e4 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/ISafeguardA2AContext.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/ISafeguardA2AContext.java @@ -1,8 +1,5 @@ package com.oneidentity.safeguard.safeguardjava; -import com.oneidentity.safeguard.safeguardjava.data.A2ARetrievableAccount; -import com.oneidentity.safeguard.safeguardjava.data.ApiKeySecret; -import com.oneidentity.safeguard.safeguardjava.data.BrokeredAccessRequest; import com.oneidentity.safeguard.safeguardjava.data.KeyFormat; import com.oneidentity.safeguard.safeguardjava.event.ISafeguardEventListener; import com.oneidentity.safeguard.safeguardjava.exceptions.ArgumentException; @@ -38,6 +35,15 @@ public interface ISafeguardA2AContext */ char[] retrievePassword(char[] apiKey) throws ObjectDisposedException, SafeguardForJavaException, ArgumentException; + /** + * Sets a password using Safeguard A2A. + * + * @param apiKey API key corresponding to the configured account. + * @param password Password to set. + * @return + */ + void SetPassword(char[] apiKey, char[] password) throws ObjectDisposedException, SafeguardForJavaException, ArgumentException; + /** * Retrieves an SSH private key using Safeguard A2A. * @@ -61,6 +67,17 @@ public interface ISafeguardA2AContext */ List retrieveApiKeySecret(char[] apiKey) throws ObjectDisposedException, ArgumentException, SafeguardForJavaException; + /** + * Sets an SSH private key using Safeguard A2A. + * + * @param apiKey API key corresponding to the configured account. + * @param privateKey Private key to set. + * @param password Password associated with the private key. + * @param keyFormat Format to use when returning private key. + * @return + */ + void SetPrivateKey(char[] apiKey, char[] privateKey, char[] password, KeyFormat keyFormat) throws ObjectDisposedException, ArgumentException, SafeguardForJavaException; + /** * Gets an A2A event listener. The handler passed in will be registered for the AssetAccountPasswordUpdated * event, which is the only one supported in A2A. You just have to call Start(). The event listener returned diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/SafeguardA2AContext.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/SafeguardA2AContext.java index 0ffef1c..cfb75b4 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/SafeguardA2AContext.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/SafeguardA2AContext.java @@ -9,7 +9,9 @@ import com.oneidentity.safeguard.safeguardjava.data.ApiKeySecretInternal; import com.oneidentity.safeguard.safeguardjava.data.BrokeredAccessRequest; import com.oneidentity.safeguard.safeguardjava.data.CertificateContext; +import com.oneidentity.safeguard.safeguardjava.data.JsonBody; import com.oneidentity.safeguard.safeguardjava.data.KeyFormat; +import com.oneidentity.safeguard.safeguardjava.data.SshKey; import com.oneidentity.safeguard.safeguardjava.event.ISafeguardEventListener; import com.oneidentity.safeguard.safeguardjava.event.SafeguardEventListener; import com.oneidentity.safeguard.safeguardjava.exceptions.ArgumentException; @@ -182,7 +184,42 @@ public char[] retrievePassword(char[] apiKey) throws ObjectDisposedException, Sa Logger.getLogger(SafeguardA2AContext.class.getName()).log(Level.INFO, "Successfully retrieved A2A password."); return password; } - + + @Override + public void SetPassword(char[] apiKey, char[] password) throws ObjectDisposedException, SafeguardForJavaException, ArgumentException { + if (disposed) { + throw new ObjectDisposedException("SafeguardA2AContext"); + } + + if (apiKey == null) { + throw new ArgumentException("The apiKey parameter may not be null"); + } + + if (password == null) { + throw new ArgumentException("The password parameter may not be null"); + } + + Map headers = new HashMap<>(); + headers.put(HttpHeaders.AUTHORIZATION, String.format("A2A %s", new String(apiKey))); + + Map parameters = new HashMap<>(); + + CloseableHttpResponse response = a2AClient.execPUT("Credentials/Password", parameters, headers, null, + new JsonBody("\""+new String(password)+"\""), clientCertificate); + + if (response == null) { + throw new SafeguardForJavaException(String.format("Unable to connect to web service %s", a2AClient.getBaseURL())); + } + + String reply = Utils.getResponse(response); + if (!Utils.isSuccessful(response.getStatusLine().getStatusCode())) { + throw new SafeguardForJavaException("Error returned from Safeguard API, Error: " + + String.format("%s %s", response.getStatusLine().getStatusCode(), reply)); + } + + Logger.getLogger(SafeguardA2AContext.class.getName()).log(Level.INFO, "Successfully set A2A password."); + } + @Override public char[] retrievePrivateKey(char[] apiKey, KeyFormat keyFormat) throws ObjectDisposedException, ArgumentException, SafeguardForJavaException { if (disposed) { @@ -220,6 +257,54 @@ public char[] retrievePrivateKey(char[] apiKey, KeyFormat keyFormat) throws Obje return privateKey; } + @Override + public void SetPrivateKey(char[] apiKey, char[] privateKey, char[] password, KeyFormat keyFormat) + throws ObjectDisposedException, ArgumentException, SafeguardForJavaException { + + if (disposed) { + throw new ObjectDisposedException("SafeguardA2AContext"); + } + + if (keyFormat == null) + keyFormat = KeyFormat.OpenSsh; + + if (apiKey == null) + throw new ArgumentException("The apiKey parameter may not be null."); + + if (privateKey == null) + throw new ArgumentException("The privateKey parameter may not be null"); + + if (password == null) + throw new ArgumentException("The password parameter may not be null"); + + SshKey sshKey = new SshKey(); + sshKey.setPassphrase(new String(password)); + sshKey.setPrivateKey(new String(privateKey)); + + String body = new Gson().toJson(sshKey); + + Map headers = new HashMap<>(); + headers.put(HttpHeaders.AUTHORIZATION, String.format("A2A %s", new String(apiKey))); + + Map parameters = new HashMap<>(); + parameters.put("keyFormat", keyFormat.name()); + + CloseableHttpResponse response = a2AClient.execPUT("Credentials/SshKey", parameters, headers, null, new JsonBody(body), clientCertificate); + + if (response == null) { + throw new SafeguardForJavaException(String.format("Unable to connect to web service %s", a2AClient.getBaseURL())); + } + + String reply = Utils.getResponse(response); + if (!Utils.isSuccessful(response.getStatusLine().getStatusCode())) { + throw new SafeguardForJavaException("Error returned from Safeguard API, Error: " + + String.format("%s %s", response.getStatusLine().getStatusCode(), reply)); + } + + Logger.getLogger(SafeguardA2AContext.class.getName()).log(Level.INFO, "Successfully set A2A private key."); + return; + } + @Override public List retrieveApiKeySecret(char[] apiKey) throws ObjectDisposedException, ArgumentException, SafeguardForJavaException { if (disposed) { @@ -439,5 +524,4 @@ private List parseApiKeySecretResponse(String response) { return null; } - } diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/data/SshKey.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/data/SshKey.java new file mode 100644 index 0000000..c855094 --- /dev/null +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/data/SshKey.java @@ -0,0 +1,35 @@ +package com.oneidentity.safeguard.safeguardjava.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * This class is used to set the SshKey. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SshKey { + + @JsonProperty("Passphrase") + private String passphrase; + @JsonProperty("PrivateKey") + private String privateKey; + + public SshKey() { + } + + public String getPassphrase() { + return passphrase; + } + + public void setPassphrase(String passphrase) { + this.passphrase = passphrase; + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } +} diff --git a/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java b/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java index c1e35d9..d182af5 100644 --- a/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java +++ b/src/main/java/com/oneidentity/safeguard/safeguardjava/restclient/RestClient.java @@ -293,6 +293,25 @@ public CloseableHttpResponse execPUT(String path, Map queryParam } } + public CloseableHttpResponse execPUT(String path, Map queryParams, Map headers, Integer timeout, + JsonObject requestEntity, CertificateContext certificateContext) { + CloseableHttpClient certClient = getClientWithCertificate(certificateContext); + + if (certClient != null) { + RequestBuilder rb = prepareRequest(RequestBuilder.put(getBaseURI(path)), queryParams, headers, timeout); + + try { + String body = requestEntity.toJson(); + rb.setEntity(new StringEntity(body == null ? "{}" : body)); + CloseableHttpResponse r = certClient.execute(rb.build()); + return r; + } catch (Exception ex) { + return null; + } + } + return null; + } + public CloseableHttpResponse execPOST(String path, Map queryParams, Map headers, Integer timeout, JsonObject requestEntity) { RequestBuilder rb = prepareRequest(RequestBuilder.post(getBaseURI(path)), queryParams, headers, timeout); diff --git a/tests/safeguardjavaclient/src/main/java/com/oneidentity/safeguard/safeguardclient/SafeguardTests.java b/tests/safeguardjavaclient/src/main/java/com/oneidentity/safeguard/safeguardclient/SafeguardTests.java index 703074a..9f5d07b 100644 --- a/tests/safeguardjavaclient/src/main/java/com/oneidentity/safeguard/safeguardclient/SafeguardTests.java +++ b/tests/safeguardjavaclient/src/main/java/com/oneidentity/safeguard/safeguardclient/SafeguardTests.java @@ -30,10 +30,18 @@ import com.oneidentity.safeguard.safeguardjava.exceptions.ArgumentException; import com.oneidentity.safeguard.safeguardjava.exceptions.ObjectDisposedException; import com.oneidentity.safeguard.safeguardjava.exceptions.SafeguardForJavaException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class SafeguardTests { @@ -315,6 +323,21 @@ ISafeguardA2AContext safeguardGetA2AContextByThumbprint() { return a2aContext; } + private byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream baos= new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + for (int read=0; read != -1; read = in.read(buf)) { baos.write(buf, 0, read); } + return baos.toByteArray(); + } + + private String formatPEM(String resource) throws IOException { + InputStream in = new ByteArrayInputStream(resource.getBytes()); + String pem = new String(readAllBytes(in), StandardCharsets.ISO_8859_1); + Pattern parse = Pattern.compile("(?m)(?s)^---*BEGIN.*---*$(.*)^---*END.*---*$.*"); + String encoded = parse.matcher(pem).replaceFirst("$1"); + return encoded.replace("\r", "").replace("\n", ""); + } + public void safeguardTestA2AContext(ISafeguardA2AContext a2aContext) { if (a2aContext == null) { @@ -351,19 +374,65 @@ else if (typeOfRelease.equalsIgnoreCase("a")) { System.out.println(String.format("Invalid credential release type.")); return; } + } catch (ArgumentException | ObjectDisposedException | SafeguardForJavaException ex) { + System.out.println("\t[ERROR]Test connection failed: " + ex.getMessage()); + } + } + + if (readLine("Test Setting Credential(y/n): ", "y").equalsIgnoreCase("y")) { + String typeOfRelease = readLine("Password, Private Key (p/k): ", "p"); + String apiKey = readLine("API Key: ", null); + + try { + if (typeOfRelease.equalsIgnoreCase("p")) { + String newPassword = readLine("New Password: ", ""); + a2aContext.SetPassword(apiKey.toCharArray(), newPassword.toCharArray()); + + String password = new String(a2aContext.retrievePassword(apiKey.toCharArray())); + if (password.compareTo(newPassword) == 0) + System.out.println(String.format("\tSuccessfully set password")); + else + System.out.println(String.format("\tFailed to set password")); + } + else if (typeOfRelease.equalsIgnoreCase("k")) { + String privateKeyPath = readLine("Private Key File Path: ", ""); + String privateKeyPassword = readLine("Private Key Password: ", ""); + Path filePath = Paths.get(privateKeyPath).toAbsolutePath(); + String privateKey = new String(Files.readAllBytes(filePath)); + a2aContext.SetPrivateKey(apiKey.toCharArray(), privateKey.toCharArray(), privateKeyPassword.toCharArray(), KeyFormat.OpenSsh); + + String key = new String(a2aContext.retrievePrivateKey(apiKey.toCharArray(), KeyFormat.OpenSsh)); + + String privkey1 = formatPEM(privateKey); + String privkey2 = formatPEM(key); + + if (privkey1.compareTo(privkey2) == 0) + System.out.println(String.format("\tSuccessful private key release")); + else + System.out.println(String.format("\tFailed to set private key")); + } + else { + System.out.println(String.format("Invalid credential release type.")); + return; + } + } catch (ArgumentException | ObjectDisposedException | SafeguardForJavaException | IOException ex) { + System.out.println("\t[ERROR]Test connection failed: " + ex.getMessage()); + } + } + + if (readLine("Test Access Request Broker(y/n): ", "y").equalsIgnoreCase("y")) { + try { List registrations = a2aContext.getRetrievableAccounts(); System.out.println(String.format("\tRetrievable accounts:")); for (IA2ARetrievableAccount reg : registrations) { System.out.println(String.format("\t\tAssetId: %d AssetName: %s AccountId: %d AccountName: %s AccountDescription: %s", reg.getAssetId(), reg.getAssetName(), reg.getAccountId(), reg.getAccountName(), reg.getAccountDescription())); } - } catch (ArgumentException | ObjectDisposedException | SafeguardForJavaException ex) { - System.out.println("\t[ERROR]Test connection failed: " + ex.getMessage()); + } catch (ObjectDisposedException | SafeguardForJavaException ex) { + System.out.println("\t[ERROR]Failed to get the retrievable accounts: " + ex.getMessage()); } - } - if (readLine("Test Access Request Broker(y/n): ", "y").equalsIgnoreCase("y")) { String accountId = readLine("Account Id: ", null); String assetId = readLine("Asset Id:", null); String forUserId = readLine("For User Id:", null);