diff --git a/src/main/java/com/nike/cerberus/service/AuthenticationService.java b/src/main/java/com/nike/cerberus/service/AuthenticationService.java index 01bdf5a67..29e85ba8f 100644 --- a/src/main/java/com/nike/cerberus/service/AuthenticationService.java +++ b/src/main/java/com/nike/cerberus/service/AuthenticationService.java @@ -59,14 +59,12 @@ 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; @@ -74,6 +72,8 @@ 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. */ @@ -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(); @@ -230,7 +230,7 @@ private IamRoleAuthResponse authenticate(IamPrincipalCredentials credentials, Ma throw e; } - final Set policies = buildPolicySet(credentials.getIamPrincipalArn()); + final Set policies = buildCompleteSetOfPolicies(credentials.getIamPrincipalArn()); final VaultTokenAuthRequest tokenAuthRequest = new VaultTokenAuthRequest() .setPolicies(policies) @@ -401,6 +401,28 @@ private Set buildPolicySet(final Set 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 buildCompleteSetOfPolicies(final String iamPrincipalArn) { + + final Set 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 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. @@ -428,13 +450,13 @@ private Set buildPolicySet(final String iamRoleArn) { * @return KMS Key id */ protected String getKeyId(IamPrincipalCredentials credentials) { - final Optional iamRole = awsIamRoleDao.getIamRole(credentials.getIamPrincipalArn()); + final String iamPrincipalArn = credentials.getIamPrincipalArn(); + final Optional 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(); } @@ -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; @@ -507,8 +528,13 @@ private Set getAdminRoleArnSet() { return adminRoleArnSet; } - protected Map 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 generateCommonVaultPrincipalAuthMetadata(final String iamPrincipalArn, final String region) { Map metadata = Maps.newHashMap(); metadata.put(VaultAuthPrincipal.METADATA_KEY_AWS_REGION, region); metadata.put(VaultAuthPrincipal.METADATA_KEY_USERNAME, iamPrincipalArn); @@ -527,4 +553,25 @@ protected Map 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 findIamRoleAssociatedWithSdb(final String iamPrincipalArn) { + Optional 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; + } } diff --git a/src/main/java/com/nike/cerberus/util/AwsIamRoleArnParser.java b/src/main/java/com/nike/cerberus/util/AwsIamRoleArnParser.java index 1fdca869c..72d572ad4 100644 --- a/src/main/java/com/nike/cerberus/util/AwsIamRoleArnParser.java +++ b/src/main/java/com/nike/cerberus/util/AwsIamRoleArnParser.java @@ -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::(?\\d+?):role/(?.+)$"; + public static final String AWS_IAM_PRINCIPAL_ARN_REGEX = "^arn:aws:(iam|sts)::(?\\d+?):(?!group).+?/(?.+)$"; - 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::(?\\d+?):role/(?.+)$"; + + private static final String AWS_IAM_ASSUMED_ROLE_ARN_REGEX = "^arn:aws:sts::(?\\d+?):assumed-role/(?.+)/.+$"; + + private static final String GENERIC_ASSUMED_ROLE_REGEX = "^arn:aws:sts::(?\\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); } } diff --git a/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java b/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java index 163b1babc..0b50d5276 100644 --- a/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java +++ b/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java @@ -26,6 +26,7 @@ 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; @@ -33,6 +34,8 @@ 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; @@ -40,16 +43,21 @@ 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; @@ -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"; @@ -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 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 roleArnRecords = Lists.newArrayList(roleArnRecord); + when(safeDepositBoxDao.getIamRoleAssociatedSafeDepositBoxRoles(roleArn)).thenReturn(roleArnRecords); + when(vaultPolicyService.buildPolicyName(roleArnSdb, roleName)).thenReturn(rolePolicy); + + List expectedPolicies = Lists.newArrayList(principalPolicy1, principalPolicy2, rolePolicy, LOOKUP_SELF_POLICY); + Set expected = Sets.newHashSet(expectedPolicies); + Set 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 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 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 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() diff --git a/src/test/java/com/nike/cerberus/util/AwsIamRoleArnParserTest.java b/src/test/java/com/nike/cerberus/util/AwsIamRoleArnParserTest.java index c4575486c..cd15bbbd2 100644 --- a/src/test/java/com/nike/cerberus/util/AwsIamRoleArnParserTest.java +++ b/src/test/java/com/nike/cerberus/util/AwsIamRoleArnParserTest.java @@ -25,6 +25,8 @@ import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -64,4 +66,42 @@ public void getRoleName_fails_on_invalid_arn() { awsIamRoleArnParser.getRoleName("brouhaha"); } + + @Test + public void convertPrincipalArnToRoleArn_properly_converts_principals_to_role_arns() { + + assertEquals("arn:aws:iam::1111111111:role/lamb_dev_health", awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:sts::1111111111:federated-user/lamb_dev_health")); + assertEquals("arn:aws:iam::2222222222:role/prince_role", awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:sts::2222222222:assumed-role/prince_role/session-name")); + assertEquals("arn:aws:iam::2222222222:role/sir/alfred/role", awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:sts::2222222222:assumed-role/sir/alfred/role/session-name")); + assertEquals("arn:aws:iam::3333333333:role/path/to/foo", awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:iam::3333333333:role/path/to/foo")); + assertEquals("arn:aws:iam::4444444444:role/name", awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:iam::4444444444:role/name")); + } + + @Test(expected = RuntimeException.class) + public void convertPrincipalArnToRoleArn_fails_on_invalid_arn() { + + awsIamRoleArnParser.convertPrincipalArnToRoleArn("foobar"); + } + + @Test(expected = RuntimeException.class) + public void convertPrincipalArnToRoleArn_fails_on_group_arn() { + + awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:iam::1111111111:group/path/to/group"); + } + + @Test(expected = RuntimeException.class) + public void convertPrincipalArnToRoleArn_fails_on_invalid_assumed_role_arn() { + + awsIamRoleArnParser.convertPrincipalArnToRoleArn("arn:aws:sts::1111111111:assumed-role/blah"); + } + + @Test + public void isRoleArn_returns_true_when_is_role_arn() { + + assertTrue(awsIamRoleArnParser.isRoleArn("arn:aws:iam::2222222222:role/fancy/role/path")); + assertTrue(awsIamRoleArnParser.isRoleArn("arn:aws:iam::1111111111:role/name")); + assertFalse(awsIamRoleArnParser.isRoleArn("arn:aws:iam::3333333333:assumed-role/happy/path")); + assertFalse(awsIamRoleArnParser.isRoleArn("arn:aws:sts::1111111111:federated-user/my_user")); + assertFalse(awsIamRoleArnParser.isRoleArn("arn:aws:iam::1111111111:group/path/to/group")); + } } \ No newline at end of file