Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add account number verification #14

Closed
wants to merge 5 commits into from
Closed
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
6 changes: 1 addition & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
language: java
jdk:
- oraclejdk8
- openjdk8
sudo: false
addons:
apt:
packages:
- oracle-java8-installer
install: true
script: mvn clean install
after_success:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.google.common.base.Strings;
import com.intuit.cloudraider.utils.AWSAccountValidator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -57,7 +59,19 @@ public class BasicCredentials implements Credentials{
private String AWSSecretKey;
private String AWSSessionToken;

/**
private String targetAccount;

public String getTargetAccount() {
return targetAccount;
}

public void setTargetAccount(String targetAccount) {
this.targetAccount = targetAccount;
}

private AWSAccountValidator validator;

/**
* Reads from config.properties to search for potential AWS account credentials.
* The aws.region must be specified to proceed.
* A "aws.keyless" option exists and requires "aws.assumerRole", "aws.deployerRole", "aws.uuid" to be specified within
Expand All @@ -79,7 +93,10 @@ public BasicCredentials() {
if(Strings.isNullOrEmpty(region)){
throw new RuntimeException("No Region defined in the configuration file");
}


// Get the target account from env or properties if env var is not set
targetAccount = System.getenv("aws.targetAccount") != null ? System.getenv("aws.targetAccount") : prop.getProperty("aws.targetAccount");

String profile = prop.getProperty("aws.profile");
boolean keyless = Boolean.valueOf(prop.getProperty("aws.keyless"));

Expand Down Expand Up @@ -150,14 +167,24 @@ public BasicCredentials() {
//ignore to use amazon default provider
}
}

public BasicCredentials(AWSAccountValidator validator) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a new constructor which will not be invoked by default... would recommend adding this logic in the default constructor so that all existing usages get this feature

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this so that I could inject a mock instance in order to test with. The logic in getCredentials will create an instance of AWSAccountValidator if one does not exist so existing usages will get this feature

this.validator = validator;
}

/**
* Returns AWSCredentials for the given user/account. Reverts to default credentials provider if no credentials exist.
* @return AWSCredentials
*/
public AWSCredentials getAwsCredentials() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This function is a failsafe and the actual logic of fetching the credentials(via various methods) is done in the default constructor.... I would leave this function alone and make the validator call from the default constructor...
  • Also i would pass the validator function the AWSCredentials object or pass the account number fetched from the account as validator function does not have access to credentials

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, there should be one method that creates the credentials. So I'd like to consolidate that code into one method and then put the validation logic there. Does that make sense?

if (awsCredentials == null) {
awsCredentials = new DefaultAWSCredentialsProviderChain().getCredentials();
AWSCredentialsProvider provider = new DefaultAWSCredentialsProviderChain();
if (validator == null) {
validator = new AWSAccountValidator(provider);
}

validator.validateAccount(targetAccount);
awsCredentials = provider.getCredentials();
}
return awsCredentials;
}
Expand All @@ -174,4 +201,5 @@ public String getRegion() {
return region;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.intuit.cloudraider.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest;
import com.amazonaws.services.securitytoken.model.GetCallerIdentityResult;

public class AWSAccountValidator {

/**
* The Logger.
*/
Logger logger = LoggerFactory.getLogger(this.getClass());

private AWSSecurityTokenService sts;


public AWSSecurityTokenService getSts() {
return sts;
}

public void setSts(AWSSecurityTokenService sts) {
this.sts = sts;
}

public AWSAccountValidator(AWSCredentialsProvider provider) {
sts = AWSSecurityTokenServiceClientBuilder.standard().withCredentials(provider).build();
}

public void validateAccount(String targetAccount) {

if (targetAccount != null) {
GetCallerIdentityRequest getCallerIdentityRequest = new GetCallerIdentityRequest();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                    .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                    .withRegion(region)
                    .build()
                    .getCallerIdentity(new GetCallerIdentityRequest())
                    .getAccount();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the code builds the AWSCredentialsProvider on the fly, there's no real way to unit test the code without having valid credentials (ie, no way to inject a mock provider).

GetCallerIdentityResult result = sts.getCallerIdentity(getCallerIdentityRequest);
if (!result.getAccount().equals(targetAccount.trim().replace("-", ""))) {
throw new RuntimeException(String.format("account: %s does not match target account: %s", result.getAccount(), targetAccount));
}
} else {
logger.info("No targetAccount was set; skipping AWS Account validation");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.intuit.cloudraider.model;

import org.mockito.Mockito;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.intuit.cloudraider.utils.AWSAccountValidator;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;

import org.junit.Test;

public class BasicCredentialsTest {

@Test
public void testValidateAccountNull() {
System.setProperty("aws.accessKeyId", "accessKey");
System.setProperty("aws.secretKey", "secret");
AWSAccountValidator mockValidator = Mockito.mock(AWSAccountValidator.class);
BasicCredentials creds = new BasicCredentials(mockValidator);

creds.getAwsCredentials();

verify(mockValidator).validateAccount(null);

}

@Test
public void testValidateAccount() {
System.setProperty("aws.accessKeyId", "accessKey");
System.setProperty("aws.secretKey", "secret");
AWSAccountValidator mockValidator = Mockito.mock(AWSAccountValidator.class);
BasicCredentials creds = new BasicCredentials(mockValidator);
creds.setTargetAccount("1234");

creds.getAwsCredentials();

verify(mockValidator).validateAccount("1234");

}


@Test
public void testValidateAccountInvalid() {
AWSAccountValidator mockValidator = Mockito.mock(AWSAccountValidator.class);
BasicCredentials creds = new BasicCredentials(mockValidator);
creds.setTargetAccount("1234");
RuntimeException expected = new RuntimeException();

doThrow(expected).when(mockValidator).validateAccount("1234");

try {
creds.getAwsCredentials();
fail("getAwsCredentials did not throw an exception");
} catch(RuntimeException e) {
assertSame(expected, e);
}


}

@Test
public void testSetTargetAccountViaSystemProperties() {
System.setProperty("aws.targetAccount", "76543");
BasicCredentials creds = new BasicCredentials();
assertEquals("76543", creds.getTargetAccount());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.intuit.cloudraider.utils;

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest;
import com.amazonaws.services.securitytoken.model.GetCallerIdentityResult;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;

import org.junit.Test;

public class AWSAccountValidatorTest {

@Test
public void validateAccountNullTest() {

AWSCredentialsProvider mockProvider = Mockito.mock(AWSCredentialsProvider.class);
AWSAccountValidator validator = new AWSAccountValidator(mockProvider);

AWSSecurityTokenService mockSts = Mockito.mock(AWSSecurityTokenService.class);

assertNotNull(validator.getSts());

validator.setSts(mockSts);

validator.validateAccount(null);

verifyZeroInteractions(mockSts);

}

@Test
public void validateAccountNotValid() {

AWSCredentialsProvider mockProvider = Mockito.mock(AWSCredentialsProvider.class);
AWSAccountValidator validator = new AWSAccountValidator(mockProvider);

AWSSecurityTokenService mockSts = Mockito.mock(AWSSecurityTokenService.class);

assertNotNull(validator.getSts());

validator.setSts(mockSts);

when(mockSts.getCallerIdentity((GetCallerIdentityRequest) notNull())).thenAnswer(new Answer<GetCallerIdentityResult>() {

@Override
public GetCallerIdentityResult answer(InvocationOnMock invocation) throws Throwable {
GetCallerIdentityResult result = new GetCallerIdentityResult();
result.setAccount("5678");
return result;
}

});

try {
validator.validateAccount("1234");
fail("validateAccount did not throw an exception");
} catch (RuntimeException e) {
assertEquals("account: 5678 does not match target account: 1234", e.getMessage());
}
}

@Test
public void validateAccountValid() {

AWSCredentialsProvider mockProvider = Mockito.mock(AWSCredentialsProvider.class);
AWSAccountValidator validator = new AWSAccountValidator(mockProvider);

AWSSecurityTokenService mockSts = Mockito.mock(AWSSecurityTokenService.class);

assertNotNull(validator.getSts());

validator.setSts(mockSts);

when(mockSts.getCallerIdentity((GetCallerIdentityRequest) notNull())).thenAnswer(new Answer<GetCallerIdentityResult>() {

@Override
public GetCallerIdentityResult answer(InvocationOnMock invocation) throws Throwable {
GetCallerIdentityResult result = new GetCallerIdentityResult();
result.setAccount("1234");
return result;
}

});

validator.validateAccount("1234");
}

@Test
public void validateAccountValidWithDashes() {

AWSCredentialsProvider mockProvider = Mockito.mock(AWSCredentialsProvider.class);
AWSAccountValidator validator = new AWSAccountValidator(mockProvider);

AWSSecurityTokenService mockSts = Mockito.mock(AWSSecurityTokenService.class);

assertNotNull(validator.getSts());

validator.setSts(mockSts);

when(mockSts.getCallerIdentity((GetCallerIdentityRequest) notNull())).thenAnswer(new Answer<GetCallerIdentityResult>() {

@Override
public GetCallerIdentityResult answer(InvocationOnMock invocation) throws Throwable {
GetCallerIdentityResult result = new GetCallerIdentityResult();
result.setAccount("1234");
return result;
}

});

validator.validateAccount("12-3-4-");
}

@Test
public void validateAccountValidWithWhitespace() {

AWSCredentialsProvider mockProvider = Mockito.mock(AWSCredentialsProvider.class);
AWSAccountValidator validator = new AWSAccountValidator(mockProvider);

AWSSecurityTokenService mockSts = Mockito.mock(AWSSecurityTokenService.class);

assertNotNull(validator.getSts());

validator.setSts(mockSts);

when(mockSts.getCallerIdentity((GetCallerIdentityRequest) notNull())).thenAnswer(new Answer<GetCallerIdentityResult>() {

@Override
public GetCallerIdentityResult answer(InvocationOnMock invocation) throws Throwable {
GetCallerIdentityResult result = new GetCallerIdentityResult();
result.setAccount("1234");
return result;
}

});

validator.validateAccount("12-3-4- ");
}

}
9 changes: 9 additions & 0 deletions java-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ if you are using passphrase
```
aws.ec2.privateKeyPassPhrase=
```

if you want to validate that the credentials you configured are for the expected AWS Account you can set the property

```
aws.targetAccount=<AWS Account Number>
```

either in the config.properties file, a System Property or an Environment Variable

## How to add new Test case?

Let's look an example EC2FMEATest class that invokes termination of EC2 Instance.
Expand Down