Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Enable all principals to authenticate with Cerberus (#41)
Browse files Browse the repository at this point in the history
Enable all principals to authenticate with Cerberus.
  • Loading branch information
sdford authored and fieldju committed May 31, 2017
1 parent 53bfae5 commit 215cfc1
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 27 deletions.
71 changes: 59 additions & 12 deletions src/main/java/com/nike/cerberus/service/AuthenticationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,21 @@
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.joda.time.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static com.nike.cerberus.util.AwsIamRoleArnParser.AWS_IAM_ROLE_ARN_TEMPLATE;

/**
* Authentication service for Users and IAM roles to be able to authenticate and get an assigned Vault token.
*/
Expand Down Expand Up @@ -189,7 +189,7 @@ public AuthResponse mfaCheck(final MfaCheckRequest mfaCheckRequest) {
*/
public IamRoleAuthResponse authenticate(IamRoleCredentials credentials) {

final String iamPrincipalArn = String.format(AwsIamRoleArnParser.AWS_IAM_ROLE_ARN_TEMPLATE, credentials.getAccountId(),
final String iamPrincipalArn = String.format(AWS_IAM_ROLE_ARN_TEMPLATE, credentials.getAccountId(),
credentials.getRoleName());
final String region = credentials.getRegion();

Expand Down Expand Up @@ -230,7 +230,7 @@ private IamRoleAuthResponse authenticate(IamPrincipalCredentials credentials, Ma
throw e;
}

final Set<String> policies = buildPolicySet(credentials.getIamPrincipalArn());
final Set<String> policies = buildCompleteSetOfPolicies(credentials.getIamPrincipalArn());

final VaultTokenAuthRequest tokenAuthRequest = new VaultTokenAuthRequest()
.setPolicies(policies)
Expand Down Expand Up @@ -401,6 +401,28 @@ private Set<String> buildPolicySet(final Set<String> groups) {
return policies;
}

/**
* Builds the policy set with permissions given to the specific IAM principal
* (e.g. arn:aws:iam::1111111111:instance-profile/example), as well as the base role that is assumed by that
* principal (i.e. arn:aws:iam::1111111111:role/example)
* @param iamPrincipalArn - The given IAM principal ARN during authentication
* @return - List of all policies the given ARN has access to
*/
protected Set<String> buildCompleteSetOfPolicies(final String iamPrincipalArn) {

final Set<String> allPolicies = buildPolicySet(iamPrincipalArn);

if (! awsIamRoleArnParser.isRoleArn(iamPrincipalArn)) {
logger.debug("Detected non-role ARN, attempting to collect policies for the principal's base role...");
final String iamPrincipalInRoleFormat = awsIamRoleArnParser.convertPrincipalArnToRoleArn(iamPrincipalArn);

final Set<String> additionalPolicies = buildPolicySet(iamPrincipalInRoleFormat);
allPolicies.addAll(additionalPolicies);
}

return allPolicies;
}

/**
* Builds the policy set to be associated with the to-be generated Vault token. The lookup-self policy is
* included by default. All other associated policies are based on what permissions are granted to the IAM role.
Expand Down Expand Up @@ -428,13 +450,13 @@ private Set<String> buildPolicySet(final String iamRoleArn) {
* @return KMS Key id
*/
protected String getKeyId(IamPrincipalCredentials credentials) {
final Optional<AwsIamRoleRecord> iamRole = awsIamRoleDao.getIamRole(credentials.getIamPrincipalArn());
final String iamPrincipalArn = credentials.getIamPrincipalArn();
final Optional<AwsIamRoleRecord> iamRole = findIamRoleAssociatedWithSdb(iamPrincipalArn);

if (!iamRole.isPresent()) {
throw ApiException.newBuilder()
.withApiErrors(DefaultApiError.AUTH_IAM_PRINCIPAL_INVALID)
.withExceptionMessage(String.format("The role: %s was not configured for any SDB",
credentials.getIamPrincipalArn()))
.withExceptionMessage(String.format("The role: %s was not configured for any SDB", iamPrincipalArn))
.build();
}

Expand All @@ -445,12 +467,11 @@ protected String getKeyId(IamPrincipalCredentials credentials) {
final OffsetDateTime now = dateTimeSupplier.get();

if (!kmsKey.isPresent()) {
kmsKeyId = kmsService.provisionKmsKey(iamRole.get().getId(), credentials.getIamPrincipalArn(),
credentials.getRegion(), SYSTEM_USER, now);
kmsKeyId = kmsService.provisionKmsKey(iamRole.get().getId(), iamPrincipalArn, credentials.getRegion(), SYSTEM_USER, now);
} else {
kmsKeyRecord = kmsKey.get();
kmsKeyId = kmsKeyRecord.getAwsKmsKeyId();
kmsService.validatePolicy(kmsKeyRecord, credentials.getIamPrincipalArn());
kmsService.validatePolicy(kmsKeyRecord, iamPrincipalArn);
}

return kmsKeyId;
Expand Down Expand Up @@ -507,8 +528,13 @@ private Set<String> getAdminRoleArnSet() {
return adminRoleArnSet;
}

protected Map<String, String> generateCommonVaultPrincipalAuthMetadata(String iamPrincipalArn, String region) {

/**
* Generate map of Vault token metadata that is common to all principals
* @param iamPrincipalArn - The authenticating IAM principal ARN
* @param region - The AWS region
* @return - Map of token metadata
*/
protected Map<String, String> generateCommonVaultPrincipalAuthMetadata(final String iamPrincipalArn, final String region) {
Map<String, String> metadata = Maps.newHashMap();
metadata.put(VaultAuthPrincipal.METADATA_KEY_AWS_REGION, region);
metadata.put(VaultAuthPrincipal.METADATA_KEY_USERNAME, iamPrincipalArn);
Expand All @@ -527,4 +553,25 @@ protected Map<String, String> generateCommonVaultPrincipalAuthMetadata(String ia

return metadata;
}

/**
* Search for the given IAM principal (e.g. arn:aws:iam::1111111111:instance-profile/example), if not found, then
* also search for the base role that the principal assumes (i.e. arn:aws:iam::1111111111:role/example)
* @param iamPrincipalArn - The authenticating IAM principal ARN
* @return - The associated IAM role record
*/
protected Optional<AwsIamRoleRecord> findIamRoleAssociatedWithSdb(final String iamPrincipalArn) {
Optional<AwsIamRoleRecord> iamRole = awsIamRoleDao.getIamRole(iamPrincipalArn);

// if the arn is not already in 'role' format, and cannot be found,
// then try checking for the generic "arn:aws:iam::0000000000:role/foo" format
if (!iamRole.isPresent() && !awsIamRoleArnParser.isRoleArn(iamPrincipalArn) ) {
logger.debug("Detected non-role ARN, attempting to find SDBs associated with the principal's base role...");
final String iamPrincipalInRoleFormat = awsIamRoleArnParser.convertPrincipalArnToRoleArn(iamPrincipalArn);

iamRole = awsIamRoleDao.getIamRole(iamPrincipalInRoleFormat);
}

return iamRole;
}
}
79 changes: 65 additions & 14 deletions src/main/java/com/nike/cerberus/util/AwsIamRoleArnParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,91 @@
/**
* Utility class for concatenating and parsing AWS IAM role ARNs.
*/
// TODO: remove
public class AwsIamRoleArnParser {

public static final String AWS_IAM_ROLE_ARN_TEMPLATE = "arn:aws:iam::%s:role/%s";

public static final String AWS_IAM_ROLE_ARN_REGEX = "^arn:aws:iam::(?<accountId>\\d+?):role/(?<roleName>.+)$";
public static final String AWS_IAM_PRINCIPAL_ARN_REGEX = "^arn:aws:(iam|sts)::(?<accountId>\\d+?):(?!group).+?/(?<roleName>.+)$";

public static final String AWS_IAM_PRINCIPAL_ARN_REGEX = "^arn:aws:(iam|sts)::.+$";
private static final String AWS_IAM_ROLE_ARN_REGEX = "^arn:aws:iam::(?<accountId>\\d+?):role/(?<roleName>.+)$";

private static final String AWS_IAM_ASSUMED_ROLE_ARN_REGEX = "^arn:aws:sts::(?<accountId>\\d+?):assumed-role/(?<roleName>.+)/.+$";

private static final String GENERIC_ASSUMED_ROLE_REGEX = "^arn:aws:sts::(?<accountId>\\d+?):assumed-role/.+$";

private static final Pattern IAM_PRINCIPAL_ARN_PATTERN = Pattern.compile(AWS_IAM_PRINCIPAL_ARN_REGEX);

private static final Pattern IAM_ROLE_ARN_PATTERN = Pattern.compile(AWS_IAM_ROLE_ARN_REGEX);

public String getAccountId(String roleArn) {
private static final Pattern IAM_ASSUMED_ROLE_ARN_PATTERN = Pattern.compile(AWS_IAM_ASSUMED_ROLE_ARN_REGEX);

Matcher iamRoleArnMatcher = IAM_ROLE_ARN_PATTERN.matcher(roleArn);
private static final Pattern GENERIC_ASSUMED_ROLE_PATTERN = Pattern.compile(GENERIC_ASSUMED_ROLE_REGEX);

if (! iamRoleArnMatcher.find()) {
throw ApiException.newBuilder()
.withApiErrors(new InvalidIamRoleArnApiError(roleArn))
.build();
/**
* Gets account ID from a 'role' ARN
* @param roleArn - Role ARN to parse
* @return - Account ID
*/
public String getAccountId(final String roleArn) {

return getNamedGroupFromRegexPattern(IAM_ROLE_ARN_PATTERN, "accountId", roleArn);
}

/**
* Gets role name form a 'role' ARN
* @param roleArn - Role ARN to parse
* @return
*/
public String getRoleName(final String roleArn) {

return getNamedGroupFromRegexPattern(IAM_ROLE_ARN_PATTERN, "roleName", roleArn);

}

/**
* Returns true if the ARN is in format 'arn:aws:iam::000000000:role/example' and false if not
* @param arn - ARN to test
* @return - True if is 'role' ARN, False if not
*/
public boolean isRoleArn(final String arn) {

final Matcher iamRoleArnMatcher = IAM_ROLE_ARN_PATTERN.matcher(arn);

return iamRoleArnMatcher.find();
}

/**
* Converts a principal ARN (e.g. 'arn:aws:iam::0000000000:instance-profile/example') to a role ARN,
* (i.e. 'arn:aws:iam::000000000:role/example')
* @param principalArn - Principal ARN to convert
* @return - Role ARN
*/
public String convertPrincipalArnToRoleArn(final String principalArn) {

if (isRoleArn(principalArn)) {
return principalArn;
}

return iamRoleArnMatcher.group("accountId");
final boolean isAssumedRole = GENERIC_ASSUMED_ROLE_PATTERN.matcher(principalArn).find();
final Pattern patternToMatch = isAssumedRole ? IAM_ASSUMED_ROLE_ARN_PATTERN : IAM_PRINCIPAL_ARN_PATTERN;

final String accountId = getNamedGroupFromRegexPattern(patternToMatch, "accountId", principalArn);
final String roleName = getNamedGroupFromRegexPattern(patternToMatch, "roleName", principalArn);

return String.format(AWS_IAM_ROLE_ARN_TEMPLATE, accountId, roleName);
}

public String getRoleName(String roleArn) {

Matcher iamRoleArnMatcher = IAM_ROLE_ARN_PATTERN.matcher(roleArn);
private String getNamedGroupFromRegexPattern(final Pattern pattern, final String groupName, final String input) {
final Matcher iamRoleArnMatcher = pattern.matcher(input);

if (! iamRoleArnMatcher.find()) {
throw ApiException.newBuilder()
.withApiErrors(new InvalidIamRoleArnApiError(roleArn))
.withApiErrors(new InvalidIamRoleArnApiError(input))
.withExceptionMessage("ARN does not match pattern: " + pattern.toString())
.build();
}

return iamRoleArnMatcher.group("roleName");
return iamRoleArnMatcher.group(groupName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,38 @@
import com.nike.cerberus.domain.IamPrincipalCredentials;
import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord;
import com.nike.cerberus.record.AwsIamRoleRecord;
import com.nike.cerberus.record.SafeDepositBoxRoleRecord;
import com.nike.cerberus.security.VaultAuthPrincipal;
import com.nike.cerberus.server.config.CmsConfig;
import com.nike.cerberus.util.AwsIamRoleArnParser;
import com.nike.cerberus.util.DateTimeSupplier;
import com.nike.vault.client.VaultAdminClient;
import com.nike.vault.client.model.VaultAuthResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Sets;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import static com.nike.cerberus.service.AuthenticationService.LOOKUP_SELF_POLICY;
import static com.nike.cerberus.util.AwsIamRoleArnParser.AWS_IAM_ROLE_ARN_TEMPLATE;
import static org.junit.Assert.assertEquals;

import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -121,7 +129,7 @@ public void tests_that_generateCommonVaultPrincipalAuthMetadata_contains_expecte
}

@Test
public void tests_that_getKeyId_only_validates_kms_policy_one_time_within_interval() {
public void test_that_getKeyId_only_validates_kms_policy_one_time_within_interval() {

String principalArn = "principal arn";
String region = "region";
Expand Down Expand Up @@ -158,6 +166,93 @@ public void tests_that_getKeyId_only_validates_kms_policy_one_time_within_interv
verify(kmsService, times(1)).validatePolicy(awsIamRoleKmsKeyRecord, principalArn);
}

@Test
public void test_that_buildCompleteSetOfPolicies_returns_all_policies() {

String accountId = "0000000000";
String roleName = "role/path";
String principalArn = String.format("arn:aws:iam::%s:instance-profile/%s", accountId, roleName);

String roleArn = String.format(AWS_IAM_ROLE_ARN_TEMPLATE, accountId, roleName);
when(awsIamRoleArnParser.isRoleArn(principalArn)).thenReturn(false);
when(awsIamRoleArnParser.convertPrincipalArnToRoleArn(principalArn)).thenReturn(roleArn);

String principalPolicy1 = "principal policy 1";
String principalPolicy2 = "principal policy 2";
String principalArnSdb1 = "principal arn sdb 1";
String principalArnSdb2 = "principal arn sdb 2";
SafeDepositBoxRoleRecord principalArnRecord1 = new SafeDepositBoxRoleRecord().setRoleName(roleName).setSafeDepositBoxName(principalArnSdb1);
SafeDepositBoxRoleRecord principalArnRecord2 = new SafeDepositBoxRoleRecord().setRoleName(roleName).setSafeDepositBoxName(principalArnSdb2);
List<SafeDepositBoxRoleRecord> principalArnRecords = Lists.newArrayList(principalArnRecord1, principalArnRecord2);
when(safeDepositBoxDao.getIamRoleAssociatedSafeDepositBoxRoles(principalArn)).thenReturn(principalArnRecords);
when(vaultPolicyService.buildPolicyName(principalArnSdb1, roleName)).thenReturn(principalPolicy1);
when(vaultPolicyService.buildPolicyName(principalArnSdb2, roleName)).thenReturn(principalPolicy2);

String rolePolicy = "role policy";
String roleArnSdb = "role arn sdb";
SafeDepositBoxRoleRecord roleArnRecord = new SafeDepositBoxRoleRecord().setRoleName(roleName).setSafeDepositBoxName(roleArnSdb);
List<SafeDepositBoxRoleRecord> roleArnRecords = Lists.newArrayList(roleArnRecord);
when(safeDepositBoxDao.getIamRoleAssociatedSafeDepositBoxRoles(roleArn)).thenReturn(roleArnRecords);
when(vaultPolicyService.buildPolicyName(roleArnSdb, roleName)).thenReturn(rolePolicy);

List<String> expectedPolicies = Lists.newArrayList(principalPolicy1, principalPolicy2, rolePolicy, LOOKUP_SELF_POLICY);
Set<String> expected = Sets.newHashSet(expectedPolicies);
Set<String> result = authenticationService.buildCompleteSetOfPolicies(principalArn);

assertEquals(expected, result);
}

@Test
public void test_that_findIamRoleAssociatedWithSdb_returns_first_matching_iam_role_record_if_found() {

String principalArn = "principal arn";
AwsIamRoleRecord awsIamRoleRecord = mock(AwsIamRoleRecord.class);
when(awsIamRoleDao.getIamRole(principalArn)).thenReturn(Optional.of(awsIamRoleRecord));

Optional<AwsIamRoleRecord> result = authenticationService.findIamRoleAssociatedWithSdb(principalArn);

assertEquals(awsIamRoleRecord, result.get());
}

@Test
public void test_that_findIamRoleAssociatedWithSdb_returns_generic_role_when_iam_principal_not_found() {

String accountId = "0000000000";
String roleName = "role/path";
String principalArn = String.format("arn:aws:iam::%s:instance-profile/%s", accountId, roleName);
String roleArn = String.format(AWS_IAM_ROLE_ARN_TEMPLATE, accountId, roleName);

AwsIamRoleRecord awsIamRoleRecord = mock(AwsIamRoleRecord.class);
when(awsIamRoleDao.getIamRole(principalArn)).thenReturn(Optional.empty());
when(awsIamRoleDao.getIamRole(roleArn)).thenReturn(Optional.of(awsIamRoleRecord));

when(awsIamRoleArnParser.isRoleArn(principalArn)).thenReturn(false);
when(awsIamRoleArnParser.convertPrincipalArnToRoleArn(principalArn)).thenReturn(roleArn);

Optional<AwsIamRoleRecord> result = authenticationService.findIamRoleAssociatedWithSdb(principalArn);

assertEquals(awsIamRoleRecord, result.get());
}

@Test
public void test_that_findIamRoleAssociatedWithSdb_returns_empty_optional_when_roles_not_found() {

String accountId = "0000000000";
String roleName = "role/path";
String principalArn = String.format("arn:aws:iam::%s:instance-profile/%s", accountId, roleName);
String roleArn = String.format("arn:aws:iam::%s:role/%s", accountId, roleName);

when(awsIamRoleDao.getIamRole(principalArn)).thenReturn(Optional.empty());
when(awsIamRoleDao.getIamRole(roleArn)).thenReturn(Optional.empty());

when(awsIamRoleArnParser.isRoleArn(principalArn)).thenReturn(false);
when(awsIamRoleArnParser.convertPrincipalArnToRoleArn(principalArn)).thenReturn(roleArn);

Optional<AwsIamRoleRecord> result = authenticationService.findIamRoleAssociatedWithSdb(principalArn);

assertFalse(result.isPresent());
}

@Test
public void tests_that_validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize_returns_the_original_payload_if_the_size_can_be_encrypted_by_kms() throws JsonProcessingException {
VaultAuthResponse response = new VaultAuthResponse()
Expand Down
Loading

0 comments on commit 215cfc1

Please sign in to comment.