diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 07e09f9..71a9808 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -387,10 +387,14 @@ class ApiCall { if (null != apiPath.getAccept()) { request.setHeader("Accept", apiPath.getAccept()); } - WriteOutHandler handler = new WriteOutHandler(outputStream); - int wrote = execute(request, handler); - if(handler.thrown!=null){ - throw handler.thrown; + final WriteOutHandler writeOutHandler = new WriteOutHandler(outputStream); + Handler handler = writeOutHandler; + if(null!=apiPath.getRequiredContentType()){ + handler = new RequireContentTypeHandler(apiPath.getRequiredContentType(), handler); + } + final int wrote = execute(request, handler); + if(writeOutHandler.thrown!=null){ + throw writeOutHandler.thrown; } return wrote; } @@ -435,6 +439,51 @@ class ApiCall { } } + /** + * Handles writing response to an output stream + */ + private static class ChainHandler implements Handler { + Handler chain; + private ChainHandler(Handler chain) { + this.chain=chain; + } + @Override + public T handle(final HttpResponse response) { + return chain.handle(response); + } + } + + /** + * Handles writing response to an output stream + */ + private static class RequireContentTypeHandler extends ChainHandler { + String contentType; + + private RequireContentTypeHandler(final String contentType, final Handler chain) { + super(chain); + this.contentType = contentType; + } + + @Override + public T handle(final HttpResponse response) { + final Header firstHeader = response.getFirstHeader("Content-Type"); + final String[] split = firstHeader.getValue().split(";"); + boolean matched=false; + for (int i = 0; i < split.length; i++) { + String s = split[i]; + if (this.contentType.equalsIgnoreCase(s.trim())) { + matched=true; + break; + } + } + if(!matched) { + throw new RundeckApiException.RundeckApiHttpContentTypeException(firstHeader.getValue(), + this.contentType); + } + return super.handle(response); + } + } + /** * Handles writing response to an output stream */ diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java index 05250ec..7ef79ee 100644 --- a/src/main/java/org/rundeck/api/ApiPathBuilder.java +++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java @@ -50,6 +50,7 @@ class ApiPathBuilder { private InputStream contentStream; private File contentFile; private String contentType; + private String requiredContentType; private boolean emptyContent = false; /** Marker for using the right separator between parameters ("?" or "&") */ @@ -416,6 +417,16 @@ class ApiPathBuilder { public boolean isEmptyContent() { return emptyContent; } + + public ApiPathBuilder requireContentType(String contentType) { + this.requiredContentType=contentType; + return this; + } + + public String getRequiredContentType() { + return requiredContentType; + } + /** * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder} * diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java index 2a5b1ca..5a2de8b 100644 --- a/src/main/java/org/rundeck/api/RundeckApiException.java +++ b/src/main/java/org/rundeck/api/RundeckApiException.java @@ -105,4 +105,42 @@ public class RundeckApiException extends RuntimeException { } } + /** + * Error due to unexpected HTTP content-type + */ + public static class RundeckApiHttpContentTypeException extends RundeckApiAuthException { + + private static final long serialVersionUID = 1L; + private String contentType; + private String requiredContentType; + + public RundeckApiHttpContentTypeException(final String contentType, + final String requiredContentType) { + super("Unexpected content-type: '" + contentType + "', expected: '" + requiredContentType + "'"); + this.contentType = contentType; + this.requiredContentType = requiredContentType; + } + public RundeckApiHttpContentTypeException(final String message, final String contentType, + final String requiredContentType) { + super(message); + this.contentType = contentType; + this.requiredContentType = requiredContentType; + } + + public RundeckApiHttpContentTypeException(final String message, final Throwable cause, final String contentType, + final String requiredContentType) { + super(message, cause); + this.contentType = contentType; + this.requiredContentType = requiredContentType; + } + + public String getContentType() { + return contentType; + } + + public String getRequiredContentType() { + return requiredContentType; + } + } + } diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index e56bda1..b0baa4c 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -19,8 +19,6 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.dom4j.Document; -import org.dom4j.DocumentFactory; -import org.dom4j.Element; import org.rundeck.api.RundeckApiException.RundeckApiLoginException; import org.rundeck.api.RundeckApiException.RundeckApiTokenException; import org.rundeck.api.domain.*; @@ -83,6 +81,8 @@ public class RundeckClient implements Serializable { private static final long serialVersionUID = 1L; public static final String JOBS_IMPORT = "/jobs/import"; + public static final String STORAGE_ROOT_PATH = "/storage/"; + public static final String SSH_KEY_PATH = "ssh-key/"; /** * Supported version numbers @@ -2374,6 +2374,139 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(new ApiPathBuilder("/token/", token), new RundeckTokenParser("/token")); } + /** + * Store an SSH key file + * @param path ssh key storage path, must start with "ssh-key/" + * @param keyfile key file + * @param privateKey true to store private key, false to store public key + * @return the SSH key resource + * @throws RundeckApiException + */ + public SSHKeyResource storeSshKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{ + AssertUtil.notNull(path, "path is mandatory to store an SSH key."); + AssertUtil.notNull(keyfile, "keyfile is mandatory to store an SSH key."); + if (!path.startsWith(SSH_KEY_PATH)) { + throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH); + } + return new ApiCall(this).post( + new ApiPathBuilder(STORAGE_ROOT_PATH, path).content( + privateKey ? "application/octet-stream" : "application/pgp-keys", + keyfile + ), + new SSHKeyResourceParser("/resource") + ); + } + + /** + * Get metadata for an SSH key file + * + * @param path ssh key storage path, must start with "ssh-key/" + * + * @return the ssh key resource + * + * @throws RundeckApiException if there is an error, or if the path is a directory not a file + */ + public SSHKeyResource getSshKey(final String path) throws RundeckApiException { + AssertUtil.notNull(path, "path is mandatory to get an SSH key."); + if (!path.startsWith(SSH_KEY_PATH)) { + throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH); + } + SSHKeyResource storageResource = new ApiCall(this).get( + new ApiPathBuilder(STORAGE_ROOT_PATH, path), + new SSHKeyResourceParser("/resource") + ); + if (storageResource.isDirectory()) { + throw new RundeckApiException("SSH Key Path is a directory: " + path); + } + return storageResource; + } + + /** + * Get content for a public SSH key file + * @param path ssh key storage path, must start with "ssh-key/" + * @param out outputstream to write data to + * + * @return length of written data + * @throws RundeckApiException + */ + public int getPublicSshKeyContent(final String path, final OutputStream out) throws + RundeckApiException, IOException { + AssertUtil.notNull(path, "path is mandatory to get an SSH key."); + if (!path.startsWith(SSH_KEY_PATH)) { + throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH); + } + try { + return new ApiCall(this).get( + new ApiPathBuilder(STORAGE_ROOT_PATH, path) + .accept("application/pgp-keys") + .requireContentType("application/pgp-keys"), + out + ); + } catch (RundeckApiException.RundeckApiHttpContentTypeException e) { + throw new RundeckApiException("Requested SSH Key path was not a Public key: " + path, e); + } + } + + /** + * Get content for a public SSH key file + * @param path ssh key storage path, must start with "ssh-key/" + * @param out file to write data to + * @return length of written data + * @throws RundeckApiException + */ + public int getPublicSshKeyContent(final String path, final File out) throws + RundeckApiException, IOException { + final FileOutputStream fileOutputStream = new FileOutputStream(out); + try { + return getPublicSshKeyContent(path, fileOutputStream); + }finally { + fileOutputStream.close(); + } + } + + /** + * List contents of root SSH key directory + * + * @return list of SSH key resources + * @throws RundeckApiException + */ + public List listSshKeyDirectoryRoot() throws RundeckApiException { + return listSshKeyDirectory(SSH_KEY_PATH); + } + /** + * List contents of SSH key directory + * + * @param path ssh key storage path, must start with "ssh-key/" + * + * @throws RundeckApiException if there is an error, or if the path is a file not a directory + */ + public List listSshKeyDirectory(final String path) throws RundeckApiException { + AssertUtil.notNull(path, "path is mandatory to get an SSH key."); + if (!path.startsWith(SSH_KEY_PATH)) { + throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH); + } + SSHKeyResource storageResource = new ApiCall(this).get( + new ApiPathBuilder(STORAGE_ROOT_PATH, path), + new SSHKeyResourceParser("/resource") + ); + if(!storageResource.isDirectory()) { + throw new RundeckApiException("SSH key path is not a directory path: " + path); + } + return storageResource.getDirectoryContents(); + } + + /** + * Delete an SSH key file + * @param path a path to a SSH key file, must start with "ssh-key/" + */ + public void deleteSshKey(final String path){ + AssertUtil.notNull(path, "path is mandatory to delete an SSH key."); + if (!path.startsWith(SSH_KEY_PATH)) { + throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH); + } + new ApiCall(this).delete(new ApiPathBuilder(STORAGE_ROOT_PATH, path)); + } + /** * @return the URL of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc) */ diff --git a/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java b/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java new file mode 100644 index 0000000..b47d878 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java @@ -0,0 +1,61 @@ +package org.rundeck.api.domain; + +import org.rundeck.api.RundeckClient; +import org.rundeck.api.parser.StorageResourceParser; + +import java.util.ArrayList; +import java.util.List; + +/** + * BaseSSHKeyResource is ... + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public class BaseSSHKeyResource extends BaseStorageResource implements SSHKeyResource { + private boolean privateKey; + + public BaseSSHKeyResource() { + } + + + public boolean isPrivateKey() { + return privateKey; + } + + public void setPrivateKey(boolean privateKey) { + this.privateKey = privateKey; + } + + ArrayList sshKeyResources = new ArrayList(); + + @Override + public void setDirectoryContents(List directoryContents) { + for (StorageResource directoryContent : directoryContents) { + sshKeyResources.add(from(directoryContent)); + } + } + + @Override + public List getDirectoryContents() { + return sshKeyResources; + } + + public static BaseSSHKeyResource from(final StorageResource source) { + final BaseSSHKeyResource baseSshKeyResource = new BaseSSHKeyResource(); + baseSshKeyResource.setDirectory(source.isDirectory()); + baseSshKeyResource.setPath(source.getPath()); + baseSshKeyResource.setName(source.getName()); + baseSshKeyResource.setMetadata(source.getMetadata()); + baseSshKeyResource.setUrl(source.getUrl()); + if (!baseSshKeyResource.isDirectory()) { + baseSshKeyResource.setPrivateKey( + null != baseSshKeyResource.getMetadata() && "private".equals(baseSshKeyResource.getMetadata().get + ("Rundeck-ssh-key-type")) + ); + } else if (null != source.getDirectoryContents()) { + baseSshKeyResource.setDirectoryContents(source.getDirectoryContents()); + } + return baseSshKeyResource; + } +} diff --git a/src/main/java/org/rundeck/api/domain/BaseStorageResource.java b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java new file mode 100644 index 0000000..6639ab4 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java @@ -0,0 +1,86 @@ +package org.rundeck.api.domain; + +import java.util.List; +import java.util.Map; + +/** + * BaseStorageResource is ... + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public class BaseStorageResource implements StorageResource { + private String path; + private String url; + private String name; + private Map metadata; + private boolean directory; + private List directoryContents; + + public BaseStorageResource() { + } + + @Override + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + @Override + public boolean isDirectory() { + return directory; + } + + public void setDirectory(boolean directory) { + this.directory = directory; + } + + @Override + public List getDirectoryContents() { + return directoryContents; + } + + public void setDirectoryContents(List directoryContents) { + this.directoryContents = directoryContents; + } + + @Override + public String toString() { + return "BaseStorageResource{" + + "path='" + path + '\'' + + ", url='" + url + '\'' + + ", name='" + name + '\'' + + ", directory=" + directory + + '}'; + } +} diff --git a/src/main/java/org/rundeck/api/domain/SSHKeyResource.java b/src/main/java/org/rundeck/api/domain/SSHKeyResource.java new file mode 100644 index 0000000..33bbbd5 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/SSHKeyResource.java @@ -0,0 +1,23 @@ +package org.rundeck.api.domain; + +import java.util.List; + +/** + * SSHKeyResource represents a directory or an SSH key file + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public interface SSHKeyResource extends StorageResource { + /** + * Return true if this is a file and is a private SSH key file. + * @return + */ + public boolean isPrivateKey(); + + /** + * Return the list of SSH Key resources if this is a directory + * @return + */ + public List getDirectoryContents(); +} diff --git a/src/main/java/org/rundeck/api/domain/StorageResource.java b/src/main/java/org/rundeck/api/domain/StorageResource.java new file mode 100644 index 0000000..be02412 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/StorageResource.java @@ -0,0 +1,54 @@ +package org.rundeck.api.domain; + +import java.util.List; +import java.util.Map; + +/** + * StorageResource represents a directory or a file + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public interface StorageResource { + /** + * Return the storage path for this resource + * + * @return + */ + public String getPath(); + + /** + * Return the URL for this resource + * + * @return + */ + public String getUrl(); + + /** + * Return the file name if this is a file + * + * @return + */ + public String getName(); + + /** + * Return the metadata for this file if this is a file + * + * @return + */ + public Map getMetadata(); + + /** + * Return true if this is a directory, false if this is a file + * + * @return + */ + public boolean isDirectory(); + + /** + * Return the list of directory contents if this is a directory + * + * @return + */ + public List getDirectoryContents(); +} diff --git a/src/main/java/org/rundeck/api/parser/BaseXpathParser.java b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java new file mode 100644 index 0000000..26be541 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java @@ -0,0 +1,29 @@ +package org.rundeck.api.parser; + +import org.dom4j.Node; + +/** + * BaseXpathParser is ... + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public abstract class BaseXpathParser implements XmlNodeParser { + private String xpath; + + public BaseXpathParser() { + } + + public BaseXpathParser(String xpath) { + + this.xpath = xpath; + } + + public abstract T parse(Node node); + + @Override + public T parseXmlNode(Node node) { + Node selectedNode = xpath != null ? node.selectSingleNode(xpath) : node; + return parse(selectedNode); + } +} diff --git a/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java new file mode 100644 index 0000000..5485d31 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java @@ -0,0 +1,26 @@ +package org.rundeck.api.parser; + +import org.dom4j.Node; +import org.rundeck.api.RundeckClient; +import org.rundeck.api.domain.BaseSSHKeyResource; +import org.rundeck.api.domain.SSHKeyResource; + +/** + * SSHKeyResourceParser is ... + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public class SSHKeyResourceParser extends BaseXpathParser implements XmlNodeParser { + public SSHKeyResourceParser() { + } + + public SSHKeyResourceParser(String xpath) { + super(xpath); + } + + @Override + public SSHKeyResource parse(Node node) { + return BaseSSHKeyResource.from(new StorageResourceParser().parse(node)); + } +} diff --git a/src/main/java/org/rundeck/api/parser/StorageResourceParser.java b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java new file mode 100644 index 0000000..f1c5e6a --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java @@ -0,0 +1,61 @@ +package org.rundeck.api.parser; + +import org.dom4j.Element; +import org.dom4j.Node; +import org.rundeck.api.domain.BaseStorageResource; +import org.rundeck.api.domain.StorageResource; + +import java.util.HashMap; + +/** + * StorageResourceParser is ... + * + * @author Greg Schueler + * @since 2014-04-04 + */ +public class StorageResourceParser extends BaseXpathParser { + private BaseStorageResource holder; + public StorageResourceParser() { + + } + public StorageResourceParser(BaseStorageResource holder){ + this.holder=holder; + } + + public StorageResourceParser(String xpath) { + super(xpath); + } + + @Override + public StorageResource parse(Node node) { + String path = node.valueOf("@path"); + String type = node.valueOf("@type"); + String url = node.valueOf("@url"); + BaseStorageResource storageResource = null == holder ? new BaseStorageResource() : holder; + storageResource.setDirectory("directory".equals(type)); + storageResource.setPath(path); + storageResource.setUrl(url); + + if (storageResource.isDirectory()) { + if (node.selectSingleNode("contents") != null) { + storageResource.setDirectoryContents(new ListParser(new StorageResourceParser(), + "contents/resource").parseXmlNode(node)); + } + } else { + String name = node.valueOf("@name"); + storageResource.setName(name); + + Node meta = node.selectSingleNode("resource-meta"); + HashMap metamap = new HashMap(); + if (null != meta) { + Element metaEl = (Element) meta; + for (Object o : metaEl.elements()) { + Element sub = (Element) o; + metamap.put(sub.getName(), sub.getText().trim()); + } + } + storageResource.setMetadata(metamap); + } + return storageResource; + } +} diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 654334d..f989e69 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -1280,6 +1280,215 @@ public class RundeckClientTest { Assert.assertEquals(404, e.getStatusCode()); } } + /** + * Store ssh key + */ + @Test + @Betamax(tape = "ssh_key_store_private", mode = TapeMode.READ_ONLY) + public void storeSshKey_private() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + File temp = File.createTempFile("test-key", ".tmp"); + temp.deleteOnExit(); + FileOutputStream out = new FileOutputStream(temp); + try{ + out.write("test1".getBytes()); + }finally { + out.close(); + } + SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file1.pem", temp, true); + Assert.assertNotNull(storageResource); + Assert.assertFalse(storageResource.isDirectory()); + Assert.assertTrue(storageResource.isPrivateKey()); + Assert.assertEquals("file1.pem", storageResource.getName()); + Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem", + storageResource.getUrl()); + Assert.assertEquals(0, storageResource.getDirectoryContents().size()); + Map metadata = storageResource.getMetadata(); + Assert.assertNotNull(metadata); + Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type")); + Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type")); + } + /** + * Store ssh key + */ + @Test + @Betamax(tape = "ssh_key_store_public", mode = TapeMode.READ_ONLY) + public void storeSshKey_public() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + File temp = File.createTempFile("test-key", ".tmp"); + temp.deleteOnExit(); + FileOutputStream out = new FileOutputStream(temp); + try{ + out.write("test1".getBytes()); + }finally { + out.close(); + } + SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file2.pub", temp, false); + Assert.assertNotNull(storageResource); + Assert.assertFalse(storageResource.isDirectory()); + Assert.assertFalse(storageResource.isPrivateKey()); + Assert.assertEquals("file2.pub", storageResource.getName()); + Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub", + storageResource.getUrl()); + Assert.assertEquals(0, storageResource.getDirectoryContents().size()); + Map metadata = storageResource.getMetadata(); + Assert.assertNotNull(metadata); + Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type")); + Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type")); + } + /** + * get ssh key + */ + @Test + @Betamax(tape = "ssh_key_get_public", mode = TapeMode.READ_ONLY) + public void getSshKey_public() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file2.pub"); + Assert.assertNotNull(storageResource); + Assert.assertFalse(storageResource.isDirectory()); + Assert.assertFalse(storageResource.isPrivateKey()); + Assert.assertEquals("file2.pub", storageResource.getName()); + Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub", + storageResource.getUrl()); + Assert.assertEquals(0, storageResource.getDirectoryContents().size()); + Map metadata = storageResource.getMetadata(); + Assert.assertNotNull(metadata); + Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type")); + Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type")); + } + /** + * get ssh key + */ + @Test + @Betamax(tape = "ssh_key_get_private", mode = TapeMode.READ_ONLY) + public void getSshKey_private() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file1.pem"); + Assert.assertNotNull(storageResource); + Assert.assertFalse(storageResource.isDirectory()); + Assert.assertTrue(storageResource.isPrivateKey()); + Assert.assertEquals("file1.pem", storageResource.getName()); + Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem", + storageResource.getUrl()); + Assert.assertEquals(0, storageResource.getDirectoryContents().size()); + Map metadata = storageResource.getMetadata(); + Assert.assertNotNull(metadata); + Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type")); + Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type")); + } + /** + * get ssh key data + */ + @Test + @Betamax(tape = "ssh_key_get_data_private", mode = TapeMode.READ_ONLY) + public void getSshKeyData_private() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + File temp = File.createTempFile("test-key", ".tmp"); + temp.deleteOnExit(); + try { + int data = client.getPublicSshKeyContent("ssh-key/test/example/file1.pem", temp); + Assert.fail("expected failure"); + } catch (RundeckApiException e) { + Assert.assertEquals("Requested SSH Key path was not a Public key: ssh-key/test/example/file1.pem", + e.getMessage()); + } + } + /** + * get ssh key data + */ + @Test + @Betamax(tape = "ssh_key_get_data_public", mode = TapeMode.READ_ONLY) + public void getSshKeyData_public() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + File temp = File.createTempFile("test-key", ".tmp"); + temp.deleteOnExit(); + int length = client.getPublicSshKeyContent("ssh-key/test/example/file2.pub", temp); + Assert.assertEquals(5, length); + } + /** + * list directory + */ + @Test + @Betamax(tape = "ssh_key_list_directory", mode = TapeMode.READ_ONLY) + public void listSshKeyDirectory() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + List list = client.listSshKeyDirectory("ssh-key/test/example"); + Assert.assertEquals(2, list.size()); + SSHKeyResource storageResource1 = list.get(0); + SSHKeyResource storageResource2 = list.get(1); + + Assert.assertFalse(storageResource2.isDirectory()); + Assert.assertTrue(storageResource2.isPrivateKey()); + Assert.assertEquals("file1.pem", storageResource2.getName()); + Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource2.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem", storageResource2.getUrl()); + Assert.assertNotNull(storageResource2.getMetadata()); + + Assert.assertEquals("application/octet-stream", storageResource2.getMetadata().get("Rundeck-content-type")); + Assert.assertEquals("private", storageResource2.getMetadata().get("Rundeck-ssh-key-type")); + + Assert.assertFalse(storageResource1.isDirectory()); + Assert.assertFalse(storageResource1.isPrivateKey()); + Assert.assertEquals("file2.pub", storageResource1.getName()); + Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource1.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub", + storageResource1.getUrl()); + Assert.assertNotNull(storageResource1.getMetadata()); + Assert.assertEquals("application/pgp-keys", storageResource1.getMetadata().get("Rundeck-content-type")); + Assert.assertEquals("public", storageResource1.getMetadata().get("Rundeck-ssh-key-type")); + } + /** + * list root + */ + @Test + @Betamax(tape = "ssh_key_list_root", mode = TapeMode.READ_ONLY) + public void listSshKeyDirectoryRoot() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + List list = client.listSshKeyDirectoryRoot(); + Assert.assertEquals(2, list.size()); + SSHKeyResource storageResource0 = list.get(0); + SSHKeyResource storageResource1 = list.get(1); + + Assert.assertFalse(storageResource0.isDirectory()); + Assert.assertTrue(storageResource0.isPrivateKey()); + Assert.assertEquals("test1.pem", storageResource0.getName()); + Assert.assertEquals("ssh-key/test1.pem", storageResource0.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test1.pem", storageResource0.getUrl()); + Assert.assertNotNull(storageResource0.getMetadata()); + + Assert.assertEquals("application/octet-stream", storageResource0.getMetadata().get("Rundeck-content-type")); + Assert.assertEquals("private", storageResource0.getMetadata().get("Rundeck-ssh-key-type")); + + Assert.assertTrue(storageResource1.toString(), storageResource1.isDirectory()); + Assert.assertEquals(null, storageResource1.getName()); + Assert.assertEquals("ssh-key/test", storageResource1.getPath()); + Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test", + storageResource1.getUrl()); + Assert.assertNull(storageResource1.getMetadata()); + + + } + + /** + * delete ssh key + */ + @Test + @Betamax(tape = "ssh_key_delete", mode = TapeMode.READ_ONLY) + public void deleteSshKey() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_7, 11); + client.deleteSshKey("ssh-key/test/example/file2.pub"); + + try { + client.getSshKey("ssh-key/test/example/file2.pub"); + Assert.fail("expected failure"); + } catch (RundeckApiException.RundeckApiHttpStatusException e) { + Assert.assertEquals(404,e.getStatusCode()); + } + } @Before public void setUp() throws Exception { diff --git a/src/test/resources/betamax/tapes/ssh_key_delete.yaml b/src/test/resources/betamax/tapes/ssh_key_delete.yaml new file mode 100644 index 0000000..fd46eae --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_delete.yaml @@ -0,0 +1,36 @@ +!tape +name: ssh_key_delete +interactions: +- recorded: 2014-04-04T23:16:02.256Z + request: + method: DELETE + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub + headers: + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 204 + headers: + Content-Type: text/html;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=3rsjs6vicpc619b1uy7oshp4y;Path=/ +- recorded: 2014-04-04T23:16:02.372Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 404 + headers: + Content-Type: application/xml;charset=utf-8 + Server: Jetty(7.6.0.v20120127) + body: !!binary |- + PGVycm9yPnJlc291cmNlIG5vdCBmb3VuZDogL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YjwvZXJyb3I+ diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml b/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml new file mode 100644 index 0000000..e5e742b --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml @@ -0,0 +1,22 @@ +!tape +name: ssh_key_get_data_private +interactions: +- recorded: 2014-04-04T19:50:59.155Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem + headers: + Accept: application/pgp-keys + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: application/json;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1gzu37lkjr0fitxhf5fgkgsfu;Path=/ + body: !!binary |- + eyJwYXRoIjoic3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLXNzaC1rZXktdHlwZSI6InByaXZhdGUifX0= diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml b/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml new file mode 100644 index 0000000..1874e71 --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml @@ -0,0 +1,22 @@ +!tape +name: ssh_key_get_data_public +interactions: +- recorded: 2014-04-04T20:20:44.331Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub + headers: + Accept: application/pgp-keys + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: application/pgp-keys + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1mrub15qsorpf10cisx24h8h03;Path=/ + body: !!binary |- + dGVzdDE= diff --git a/src/test/resources/betamax/tapes/ssh_key_get_private.yaml b/src/test/resources/betamax/tapes/ssh_key_get_private.yaml new file mode 100644 index 0000000..fa0ff2c --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_get_private.yaml @@ -0,0 +1,22 @@ +!tape +name: ssh_key_get_private +interactions: +- recorded: 2014-04-04T19:47:29.880Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: application/xml;charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=nc5p0he3nw19e4gegidc4bs7;Path=/ + body: !!binary |- + PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg== diff --git a/src/test/resources/betamax/tapes/ssh_key_get_public.yaml b/src/test/resources/betamax/tapes/ssh_key_get_public.yaml new file mode 100644 index 0000000..8bede8d --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_get_public.yaml @@ -0,0 +1,22 @@ +!tape +name: ssh_key_get_public +interactions: +- recorded: 2014-04-04T19:47:29.626Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: application/xml;charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=r6p6fl87ftrb1mkwwi0pcak5i;Path=/ + body: !!binary |- + PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg== diff --git a/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml b/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml new file mode 100644 index 0000000..e49f941 --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml @@ -0,0 +1,21 @@ +!tape +name: ssh_key_list_directory +interactions: +- recorded: 2014-04-04T20:33:07.968Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: text/html;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1stwtoatsosy91ca0gcrzde698;Path=/ + body: application/pgp-keys5publicapplication/octet-stream5contentprivate diff --git a/src/test/resources/betamax/tapes/ssh_key_list_root.yaml b/src/test/resources/betamax/tapes/ssh_key_list_root.yaml new file mode 100644 index 0000000..dcefe5e --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_list_root.yaml @@ -0,0 +1,21 @@ +!tape +name: ssh_key_list_root +interactions: +- recorded: 2014-04-04T20:41:16.501Z + request: + method: GET + uri: http://rundeck.local:4440/api/11/storage/ssh-key/ + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + response: + status: 200 + headers: + Content-Type: text/html;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=5sj36vytg4y1182mziim4868b;Path=/ + body: application/octet-stream1679contentprivate diff --git a/src/test/resources/betamax/tapes/ssh_key_store_private.yaml b/src/test/resources/betamax/tapes/ssh_key_store_private.yaml new file mode 100644 index 0000000..6ef2def --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_store_private.yaml @@ -0,0 +1,25 @@ +!tape +name: ssh_key_store_private +interactions: +- recorded: 2014-04-04T19:30:35.367Z + request: + method: POST + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem + headers: + Accept: text/xml + Content-Length: '5' + Content-Type: application/octet-stream + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + body: '' + response: + status: 201 + headers: + Content-Type: application/xml;charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=ktmwc4h53xfud6v2ch67x5p9;Path=/ + body: !!binary |- + PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg== diff --git a/src/test/resources/betamax/tapes/ssh_key_store_public.yaml b/src/test/resources/betamax/tapes/ssh_key_store_public.yaml new file mode 100644 index 0000000..8a1b18a --- /dev/null +++ b/src/test/resources/betamax/tapes/ssh_key_store_public.yaml @@ -0,0 +1,25 @@ +!tape +name: ssh_key_store_public +interactions: +- recorded: 2014-04-04T19:34:02.683Z + request: + method: POST + uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub + headers: + Accept: text/xml + Content-Length: '5' + Content-Type: application/pgp-keys + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 11 + X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s + body: '' + response: + status: 201 + headers: + Content-Type: application/xml;charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=2l3g8m0tvwef19jn2bu23bzk6;Path=/ + body: !!binary |- + PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==