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 + } }