From c9e2e0c92b793845081987bd0d3d798b280a2fef Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Mon, 9 May 2022 11:56:02 -0700 Subject: [PATCH 1/6] add field for Secret Key Storage Path --- build.gradle | 2 +- .../resources/ec2/EC2ResourceModelSource.java | 35 +++++++++++++++---- .../ec2/EC2ResourceModelSourceFactory.java | 26 ++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 8db00e16..b21fa4fb 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ repositories { } dependencies { compile group:"org.slf4j", name:"slf4j-api", version:"1.7.30" - compile group: 'org.rundeck', name: 'rundeck-core', version: '2.2.2' + compile group: 'org.rundeck', name: 'rundeck-core', version: '3.4.0-20210614' compile "com.amazonaws:aws-java-sdk-core:1.11.743" compile "com.amazonaws:aws-java-sdk-sts:1.11.743" compile "com.fasterxml.jackson.core:jackson-databind:2.10.5.1" diff --git a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java index 38d8e73d..4b86633f 100644 --- a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java +++ b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java @@ -33,6 +33,10 @@ import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; import com.dtolabs.rundeck.core.resources.ResourceModelSource; import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; +import org.rundeck.app.spi.Services; +import org.rundeck.storage.api.PathUtil; +import org.rundeck.storage.api.StorageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +66,7 @@ public class EC2ResourceModelSource implements ResourceModelSource { static Logger logger = LoggerFactory.getLogger(EC2ResourceModelSource.class); private String accessKey; private String secretKey; + private String secretKeyStoragePath; long refreshInterval = 30000; long lastRefresh = 0; String filterParams; @@ -72,6 +77,7 @@ public class EC2ResourceModelSource implements ResourceModelSource { String httpProxyPass; String mappingParams; File mappingFile; + Services services; boolean useDefaultMapping = true; boolean runningOnly = false; boolean queryAsync = true; @@ -141,9 +147,10 @@ public class EC2ResourceModelSource implements ResourceModelSource { } } - public EC2ResourceModelSource(final Properties configuration) { + public EC2ResourceModelSource(final Properties configuration, final Services services) { this.accessKey = configuration.getProperty(EC2ResourceModelSourceFactory.ACCESS_KEY); this.secretKey = configuration.getProperty(EC2ResourceModelSourceFactory.SECRET_KEY); + this.secretKeyStoragePath = configuration.getProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH); this.endpoint = configuration.getProperty(EC2ResourceModelSourceFactory.ENDPOINT); this.pageResults = Integer.parseInt(configuration.getProperty(EC2ResourceModelSourceFactory.MAX_RESULTS)); this.httpProxyHost = configuration.getProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_HOST); @@ -186,13 +193,21 @@ public EC2ResourceModelSource(final Properties configuration) { EC2ResourceModelSourceFactory.RUNNING_ONLY)); logger.info("[debug] runningOnly:" + runningOnly); } - if (null != accessKey && null != secretKey) { + if (null != accessKey && null != secretKeyStoragePath) { + + KeyStorageTree keyStorage = services.getService(KeyStorageTree.class); + String secretKey = getPasswordFromKeyStorage(secretKeyStoragePath, keyStorage); + + System.out.println("SECRET KEY" + secretKey); + + credentials = new BasicAWSCredentials(accessKey.trim(), secretKey.trim()); + assumeRoleArn = null; + }else if (null != accessKey && null != secretKey) { credentials = new BasicAWSCredentials(accessKey.trim(), secretKey.trim()); assumeRoleArn = null; - }else{ + } else { assumeRoleArn = configuration.getProperty(EC2ResourceModelSourceFactory.ROLE_ARN); } - if (null != httpProxyHost && !"".equals(httpProxyHost)) { clientConfiguration.setProxyHost(httpProxyHost); clientConfiguration.setProxyPort(httpProxyPort); @@ -225,7 +240,6 @@ private void initialize() { ); } - mapper = new InstanceToNodeMapper(this.credentials, mapping, clientConfiguration, pageResults); mapper.setFilterParams(params); mapper.setEndpoint(endpoint); @@ -321,9 +335,18 @@ private void loadMapping() { } public void validate() throws ConfigurationException { - if (null != accessKey && null == secretKey) { + if (null != accessKey && null == secretKey && null == secretKeyStoragePath) { throw new ConfigurationException("secretKey is required for use with accessKey"); } + } + + static String getPasswordFromKeyStorage(String path, KeyStorageTree storage) { + try{ + String key = new String(storage.readPassword(path)); + return key; + }catch (Exception e){ + throw StorageException.readException(PathUtil.asPath(path), "error accessing key storage: ${e.message}"); + } } } diff --git a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java index 7c0c5b63..4d55b352 100644 --- a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java +++ b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java @@ -30,6 +30,7 @@ import com.dtolabs.rundeck.core.resources.ResourceModelSourceFactory; import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; import com.dtolabs.rundeck.plugins.util.PropertyBuilder; +import org.rundeck.app.spi.Services; import java.io.File; import java.util.*; @@ -59,6 +60,7 @@ public class EC2ResourceModelSourceFactory implements ResourceModelSourceFactory public static final String RUNNING_ONLY = "runningOnly"; public static final String ACCESS_KEY = "accessKey"; public static final String SECRET_KEY = "secretKey"; + public static final String SECRET_KEY_STORAGE_PATH = "secretKeyStoragePath"; public static final String ROLE_ARN = "assumeRoleArn"; public static final String MAPPING_FILE = "mappingFile"; public static final String REFRESH_INTERVAL = "refreshInterval"; @@ -74,12 +76,16 @@ public EC2ResourceModelSourceFactory(final Framework framework) { this.framework = framework; } - public ResourceModelSource createResourceModelSource(final Properties properties) throws ConfigurationException { - final EC2ResourceModelSource ec2ResourceModelSource = new EC2ResourceModelSource(properties); + public ResourceModelSource createResourceModelSource(Services services, final Properties configuration) throws ConfigurationException { + final EC2ResourceModelSource ec2ResourceModelSource = new EC2ResourceModelSource(configuration, services); ec2ResourceModelSource.validate(); return ec2ResourceModelSource; } + public ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { + return null; + } + static Description DESC = DescriptionBuilder.builder() .name(PROVIDER_NAME) .title("AWS EC2 Resources") @@ -98,6 +104,22 @@ public ResourceModelSource createResourceModelSource(final Properties properties Collections.singletonMap("displayType", (Object) StringRenderingConstants.DisplayType.PASSWORD) ) ) + .property( + PropertyUtil.string( + SECRET_KEY_STORAGE_PATH, + "Secret Key Storage Path", + "Key Storage Path to AWS Secret Key.", + false, + null, + null, + null, + new HashMap(){{ + put(StringRenderingConstants.SELECTION_ACCESSOR_KEY,StringRenderingConstants.SelectionAccessor.STORAGE_PATH); + put(StringRenderingConstants.STORAGE_PATH_ROOT_KEY,"keys"); + put(StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, "Rundeck-data-type=password"); + }} + ) + ) .property( PropertyUtil.string( ROLE_ARN, From 8cc74c67e3a7346ccc855bfe4d7c7940814ce0f0 Mon Sep 17 00:00:00 2001 From: Jake Cohen Date: Mon, 9 May 2022 15:34:58 -0700 Subject: [PATCH 2/6] cleaning up field descriptions --- .../plugin/resources/ec2/EC2ResourceModelSourceFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java index 4d55b352..fec49fc2 100644 --- a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java +++ b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java @@ -96,7 +96,7 @@ public ResourceModelSource createResourceModelSource(Properties configuration) t PropertyUtil.string( SECRET_KEY, "Secret Key", - "AWS Secret Key, required if Access Key is used. If not used, then the IAM profile will be used", + "AWS Secret Key. Required if Access Key is used and Secret Key Storage Path is blank.\nIf `Access Key` is not used, then the IAM profile will be used.", false, null, null, @@ -108,7 +108,7 @@ public ResourceModelSource createResourceModelSource(Properties configuration) t PropertyUtil.string( SECRET_KEY_STORAGE_PATH, "Secret Key Storage Path", - "Key Storage Path to AWS Secret Key.", + "Key Storage Path for AWS Secret Key. Required if Access Key is used and Secret Key is blank.\nIf `Access Key` is not used, then the IAM profile will be used.", false, null, null, From c24223c9028aa688a4b05a49e7bdd240bb5cec2e Mon Sep 17 00:00:00 2001 From: Jason Qualman <1653866+qualman@users.noreply.github.com> Date: Wed, 11 May 2022 12:02:37 -0700 Subject: [PATCH 3/6] Exclude vulnerable Guava dep --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b21fa4fb..bb21fa50 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,9 @@ repositories { } dependencies { compile group:"org.slf4j", name:"slf4j-api", version:"1.7.30" - compile group: 'org.rundeck', name: 'rundeck-core', version: '3.4.0-20210614' + compile (group: 'org.rundeck', name: 'rundeck-core', version: '3.4.0-20210614') { + exclude group: "com.google.guava" + } compile "com.amazonaws:aws-java-sdk-core:1.11.743" compile "com.amazonaws:aws-java-sdk-sts:1.11.743" compile "com.fasterxml.jackson.core:jackson-databind:2.10.5.1" From cc6ec5331a27af5e59f5be2704db3900bc2d4704 Mon Sep 17 00:00:00 2001 From: Jason Qualman <1653866+qualman@users.noreply.github.com> Date: Wed, 11 May 2022 15:59:13 -0700 Subject: [PATCH 4/6] Dont log passwords to stdout --- .gitignore | 4 ++++ .../rundeck/plugin/resources/ec2/EC2ResourceModelSource.java | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 936f9eee..19b31221 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +# IDE/Build files .gradle/ *.ipr *.iml *.iws .idea/ build/ + +# System files +**/.DS_Store diff --git a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java index 4b86633f..3e78daed 100644 --- a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java +++ b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java @@ -198,8 +198,6 @@ public EC2ResourceModelSource(final Properties configuration, final Services ser KeyStorageTree keyStorage = services.getService(KeyStorageTree.class); String secretKey = getPasswordFromKeyStorage(secretKeyStoragePath, keyStorage); - System.out.println("SECRET KEY" + secretKey); - credentials = new BasicAWSCredentials(accessKey.trim(), secretKey.trim()); assumeRoleArn = null; }else if (null != accessKey && null != secretKey) { From bdff7fd489f848991c29dfd47e621c267afa0834 Mon Sep 17 00:00:00 2001 From: Jason Qualman <1653866+qualman@users.noreply.github.com> Date: Fri, 13 May 2022 14:06:34 -0700 Subject: [PATCH 5/6] Add RecourceModelSource spec --- build.gradle | 1 + .../resources/ec2/EC2ResourceModelSource.java | 5 +- .../ec2/EC2ResourceModelSourceSpec.groovy | 99 +++++++++++++++++++ .../ec2/InstanceToNodeMapperSpec.groovy | 49 ++++----- 4 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy diff --git a/build.gradle b/build.gradle index bb21fa50..5b08d4ff 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,7 @@ dependencies { testCompile "org.codehaus.groovy:groovy-all:2.3.7" testCompile "org.spockframework:spock-core:0.7-groovy-2.0" testCompile "cglib:cglib-nodep:2.2.2" + testCompile 'org.objenesis:objenesis:3.1' } // task to copy plugin libs to output/lib dir diff --git a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java index 3e78daed..5ce95bc9 100644 --- a/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java +++ b/src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java @@ -343,7 +343,10 @@ static String getPasswordFromKeyStorage(String path, KeyStorageTree storage) { String key = new String(storage.readPassword(path)); return key; }catch (Exception e){ - throw StorageException.readException(PathUtil.asPath(path), "error accessing key storage: ${e.message}"); + throw StorageException.readException( + PathUtil.asPath(path), + "error accessing key storage at " + path + ": " + e.getMessage() + ); } } diff --git a/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy new file mode 100644 index 00000000..fb3a0804 --- /dev/null +++ b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy @@ -0,0 +1,99 @@ +package com.dtolabs.rundeck.plugin.resources.ec2 + +import com.dtolabs.rundeck.core.common.Framework +import com.dtolabs.rundeck.core.common.IRundeckProject +import com.dtolabs.rundeck.core.common.ProjectManager +import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree +import org.rundeck.app.spi.Services +import spock.lang.Specification + +class EC2ResourceModelSourceSpec extends Specification { + def "user configured access credentials prefer key storage"() { + given: "a user's plugin config" + //Define good and bad keys and paths + def validAccessKey = "validAccessKey" + def validSecretKey = "validSecretKey" + def validKeyPath = "keys/validKeyPath" + def badPath = "keys/badPath" + def badPass = "myNetflixPassword" + + // Mock services and Key Storage return of passwords + def serviceWithGoodPass = mockServicesWithPassword(validKeyPath, validSecretKey) + def serviceWithBadPass = mockServicesWithPassword(badPath, badPass) + + // Create a default config object (these are the settings the user would setup via the Plugin UI) + def defaultConfig = createDefaultConfig() + defaultConfig.setProperty(EC2ResourceModelSourceFactory.ACCESS_KEY, validAccessKey) + + // Create a working config from the defaults + def workingConfig = new Properties() + workingConfig.putAll(defaultConfig) + // Send a bad key to ensure key path takes precedence and succeeds + workingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY, badPass) + workingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, validKeyPath) + + // Create a failing config from the defaults + def failingConfig = new Properties() + failingConfig.putAll(defaultConfig) + // Send a valid key to ensure storage path takes precedence and fails + failingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY, validSecretKey) + failingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, badPath) + + // Create objects using actual ResourceModelSource and Factory + def workingRms = ec2ResourceModelSource(serviceWithGoodPass, workingConfig) + def failingRms = ec2ResourceModelSource(serviceWithBadPass, failingConfig) + + when: "we check the access keys of the resource model source objects" + // Instead of using getNodes, which would all be highly mocked, just check that we got as far as setting + // proper credentials right before the point we would call to AWS + def workingRmsPass = workingRms.credentials.getAWSSecretKey() + def failingRmsPass = failingRms.credentials.getAWSSecretKey() + + then: "we see that the proper keys from the key storage or the inline key have been derived" + workingRmsPass == validSecretKey + failingRmsPass == badPass + } + // + // Private Methods + // + private def createDefaultConfig() { + def configuration = new Properties() + def assumeRoleArn = "arn:aws:iam::123456789012:role/fake-test-arn" + def endpoint = "ALL_REGIONS" + def pageResults = "100" + def proxyPortStr = "80" + def refreshStr = "30" + def useDefaultMapping = "true" + def runningOnly = "true" + + configuration.setProperty(EC2ResourceModelSourceFactory.ROLE_ARN, assumeRoleArn) + configuration.setProperty(EC2ResourceModelSourceFactory.ENDPOINT, endpoint); + configuration.setProperty(EC2ResourceModelSourceFactory.MAX_RESULTS, pageResults); + configuration.setProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_PORT, proxyPortStr); + configuration.setProperty(EC2ResourceModelSourceFactory.REFRESH_INTERVAL, refreshStr); + configuration.setProperty(EC2ResourceModelSourceFactory.USE_DEFAULT_MAPPING, useDefaultMapping) + configuration.setProperty(EC2ResourceModelSourceFactory.RUNNING_ONLY, runningOnly) + + return configuration + } + + private def ec2ResourceModelSource(Services services, Properties configuration) { + def framework = Mock(Framework) + def factory = new EC2ResourceModelSourceFactory(framework) + + return factory.createResourceModelSource(services, configuration) + } + + private def mockServicesWithPassword(String path, String password) { + def storageTree = Mock(KeyStorageTree) { + readPassword(path) >> { + return password.bytes + } + } + + def services = Mock(Services) { + getService(KeyStorageTree.class) >> storageTree + } + + } +} diff --git a/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapperSpec.groovy b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapperSpec.groovy index 05368d0d..146d0df2 100644 --- a/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapperSpec.groovy +++ b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapperSpec.groovy @@ -160,28 +160,6 @@ class InstanceToNodeMapperSpec extends Specification { 'instanceId|tags/Name+"_"+tags/env' | null | 'aninstanceId,bob_PROD' } - private static Instance mkInstance() { - Instance i = new Instance() - i.withTags(new Tag('Name', 'bob'), new Tag('env', 'PROD')) - i.setInstanceId("aninstanceId") - i.setArchitecture("anarch") - i.setImageId("ami-something") - i.setPlacement(new Placement("us-east-1a")) - - def state = new InstanceState() - state.setName(InstanceStateName.Running) - i.setState(state) - i.setPrivateIpAddress('127.0.9.9') - return i - } - - private static Image mkImage(){ - Image image = new Image() - image.setImageId("ami-something") - image.setName("AMISomething") - return image - } - def "extra mapping image"() { given: @@ -223,8 +201,6 @@ class InstanceToNodeMapperSpec extends Specification { 'imageId+"-"+imageName' | "ami_image" } - - def "extra mapping not calling image list"() { given: @@ -301,4 +277,29 @@ class InstanceToNodeMapperSpec extends Specification { instances.getNode("aninstanceId").getAttributes().get("region") == "us-east-1" } + + // + // Private Methods + // + private static Instance mkInstance() { + Instance i = new Instance() + i.withTags(new Tag('Name', 'bob'), new Tag('env', 'PROD')) + i.setInstanceId("aninstanceId") + i.setArchitecture("anarch") + i.setImageId("ami-something") + i.setPlacement(new Placement("us-east-1a")) + + def state = new InstanceState() + state.setName(InstanceStateName.Running) + i.setState(state) + i.setPrivateIpAddress('127.0.9.9') + return i + } + + private static Image mkImage(){ + Image image = new Image() + image.setImageId("ami-something") + image.setName("AMISomething") + return image + } } From 3fbba6a5f2c71b5f3e7979fc898cb18bb4004e29 Mon Sep 17 00:00:00 2001 From: Jason Qualman <1653866+qualman@users.noreply.github.com> Date: Tue, 17 May 2022 08:59:34 -0700 Subject: [PATCH 6/6] Add test to ensure proper key storage error --- .../ec2/EC2ResourceModelSourceSpec.groovy | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy index fb3a0804..4e9c4f9b 100644 --- a/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy +++ b/src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy @@ -5,6 +5,7 @@ import com.dtolabs.rundeck.core.common.IRundeckProject import com.dtolabs.rundeck.core.common.ProjectManager import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree import org.rundeck.app.spi.Services +import org.rundeck.storage.api.StorageException import spock.lang.Specification class EC2ResourceModelSourceSpec extends Specification { @@ -53,6 +54,29 @@ class EC2ResourceModelSourceSpec extends Specification { workingRmsPass == validSecretKey failingRmsPass == badPass } + def "fail properly when invalid key path is provided"() { + given: "User plugin config that uses an invalid key path" + //Define good and bad keys and paths + def validAccessKey = "validAccessKey" + def goodKeyPath = "keys/validKeyPath" + def badPath = "keys/badPath" + def badPass = "myNetflixPassword" + + // Mock services and Key Storage return of passwords + def serviceWithBadPass = mockServicesWithPassword(badPath, null) + + // Create a default config object (these are the settings the user would setup via the Plugin UI) + def config = createDefaultConfig() + config.setProperty(EC2ResourceModelSourceFactory.ACCESS_KEY, validAccessKey) + config.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, badPath) + + when: "user attempts to create EC2ResourceModelSource instance using invalid path" + def failingRms = ec2ResourceModelSource(serviceWithBadPass, config) + + then: "expect a StorageException#readException to be returned" + StorageException ex = thrown() + ex.message.contains("error accessing key storage at ${badPath}") + } // // Private Methods //