From c82b11630294ebd77782e7fa393859a56173f6bc Mon Sep 17 00:00:00 2001 From: Justin Field Date: Mon, 22 May 2017 11:25:48 -0700 Subject: [PATCH] Bugfix/handle large policy and metadata collections (#44) * Attempt to handle tokens that have large enough policy and metadata collections that it causes KMS to fail to encrypt. --- .../service/AuthenticationService.java | 62 ++++++++++++++++++- .../service/AuthenticationServiceTest.java | 62 ++++++++++++++++--- 2 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/nike/cerberus/service/AuthenticationService.java b/src/main/java/com/nike/cerberus/service/AuthenticationService.java index 0fc6a852a..01bdf5a67 100644 --- a/src/main/java/com/nike/cerberus/service/AuthenticationService.java +++ b/src/main/java/com/nike/cerberus/service/AuthenticationService.java @@ -25,6 +25,8 @@ import com.amazonaws.services.kms.model.EncryptResult; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; @@ -87,6 +89,7 @@ public class AuthenticationService { public static final String IAM_TOKEN_TTL_OVERRIDE = "cms.iam.token.ttl.override"; public static final String LOOKUP_SELF_POLICY = "lookup-self"; public static final String DEFAULT_TOKEN_TTL = "1h"; + public static final int KMS_SIZE_LIMIT = 4096; private final SafeDepositBoxDao safeDepositBoxDao; private final AwsIamRoleDao awsIamRoleDao; @@ -235,7 +238,7 @@ private IamRoleAuthResponse authenticate(IamPrincipalCredentials credentials, Ma .setTtl(iamTokenTTL) .setNoDefaultPolicy(true); - final VaultAuthResponse authResponse = vaultAdminClient.createOrphanToken(tokenAuthRequest); + VaultAuthResponse authResponse = vaultAdminClient.createOrphanToken(tokenAuthRequest); byte[] authResponseJson; try { @@ -247,6 +250,10 @@ private IamRoleAuthResponse authenticate(IamPrincipalCredentials credentials, Ma .withExceptionMessage("Failed to write IAM role authentication response as JSON for encrypting.") .build(); } + + authResponseJson = validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize(authResponseJson, + authResponse, credentials.getIamPrincipalArn()); + final byte[] encryptedAuthResponse = encrypt(credentials.getRegion(), keyId, authResponseJson); IamRoleAuthResponse iamRoleAuthResponse = new IamRoleAuthResponse(); @@ -254,6 +261,59 @@ private IamRoleAuthResponse authenticate(IamPrincipalCredentials credentials, Ma return iamRoleAuthResponse; } + /** + * if the metadata and policies make the token too big to encrypt with KMS we can as a stop gap trim the metadata + * and policies from the token. + * + * This information is stored in Vault and can be fetched by the client with a look-up self call + * + * @param authResponseJson The current serialized auth payload + * @param authResponse The response object, with the original policies and metadata + * @param iamPrincipal The calling iam principal + * @return a serialized auth payload that KMS can encrypt + */ + protected byte[] validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize(byte[] authResponseJson, + VaultAuthResponse authResponse, + String iamPrincipal) { + + if (authResponseJson.length <= KMS_SIZE_LIMIT) { + return authResponseJson; + } + + String originalMetadata = "unknown"; + String originalPolicies = "unknown"; + try { + originalMetadata = objectMapper.writeValueAsString(authResponse.getMetadata()); + originalPolicies = objectMapper.writeValueAsString(authResponse.getPolicies()); + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize original metadata or policies for token generated for IAM Principal: {}", iamPrincipal, e); + } + + authResponse.setMetadata(ImmutableMap.of("_truncated", "true")); + authResponse.setPolicies(ImmutableSet.of("_truncated")); + + logger.warn( + "The auth token has length: {} which is > {} KMS cannot encrypt it, truncating auth payload by removing policies and metadata " + + "original metadata: {} " + + "original policies: {}", + authResponseJson.length, + KMS_SIZE_LIMIT, + originalMetadata, + originalPolicies + ); + + try { + return objectMapper.writeValueAsBytes(authResponse); + } catch (JsonProcessingException e) { + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR) + .withExceptionCause(e) + .withExceptionMessage("Failed to write IAM role authentication response as JSON for encrypting.") + .build(); + } + + } + /** * Since tokens are immutable, there are certain situations where refreshing the token used by a user is * necessary. Anytime permissions change, this is required to reflect that to the user. diff --git a/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java b/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java index be1b993f2..163b1babc 100644 --- a/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java +++ b/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java @@ -17,6 +17,7 @@ package com.nike.cerberus.service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nike.cerberus.auth.connector.AuthConnector; import com.nike.cerberus.aws.KmsClientFactory; @@ -26,25 +27,29 @@ import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord; import com.nike.cerberus.record.AwsIamRoleRecord; 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 org.joda.time.DateTime; +import com.nike.vault.client.model.VaultAuthResponse; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Before; import org.junit.Test; -import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -76,24 +81,24 @@ public class AuthenticationServiceTest { @Mock private VaultPolicyService vaultPolicyService; - @Mock private ObjectMapper objectMapper; -// @Named(ADMIN_GROUP_PROPERTY) final String adminGroup, - @Mock private DateTimeSupplier dateTimeSupplier; @Mock private AwsIamRoleArnParser awsIamRoleArnParser; - @InjectMocks private AuthenticationService authenticationService; @Before public void setup() { - initMocks(this); + objectMapper = CmsConfig.configureObjectMapper(); + authenticationService = new AuthenticationService(safeDepositBoxDao, + awsIamRoleDao, authConnector, kmsService, kmsClientFactory, + vaultAdminClient, vaultPolicyService, objectMapper, "foo", + dateTimeSupplier, awsIamRoleArnParser); } @Test @@ -153,4 +158,43 @@ public void tests_that_getKeyId_only_validates_kms_policy_one_time_within_interv verify(kmsService, times(1)).validatePolicy(awsIamRoleKmsKeyRecord, principalArn); } + @Test + public void tests_that_validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize_returns_the_original_payload_if_the_size_can_be_encrypted_by_kms() throws JsonProcessingException { + VaultAuthResponse response = new VaultAuthResponse() + .setClientToken(UUID.randomUUID().toString()) + .setLeaseDuration(3600) + .setMetadata(new HashMap<>()) + .setPolicies(new HashSet<>()) + .setRenewable(false); + + byte[] serializedAuth = new ObjectMapper().writeValueAsBytes(response); + + byte[] actual = authenticationService.validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize(serializedAuth, response, "foo"); + + assertEquals(serializedAuth, actual); + } + + @Test + public void tests_that_validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize_returns_a_truncated_payload_if_the_size_cannot_be_encrypted_by_kms() throws JsonProcessingException { + Map meta = new HashMap<>(); + Set policies = new HashSet<>(); + for (int i = 0; i < 100; i++) { + policies.add(RandomStringUtils.random(25)); + } + + VaultAuthResponse response = new VaultAuthResponse() + .setClientToken(UUID.randomUUID().toString()) + .setLeaseDuration(3600) + .setMetadata(meta) + .setPolicies(policies) + .setRenewable(false); + + byte[] serializedAuth = new ObjectMapper().writeValueAsBytes(response); + assertTrue(serializedAuth.length > AuthenticationService.KMS_SIZE_LIMIT); + + byte[] actual = authenticationService.validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSize(serializedAuth, response, "foo"); + + assertNotEquals(serializedAuth, actual); + assertTrue(actual.length < AuthenticationService.KMS_SIZE_LIMIT); + } } \ No newline at end of file