diff --git a/gradle.properties b/gradle.properties index 43394eb2a..533aa3335 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,6 @@ # limitations under the License. # -version=0.20.3 +version=0.21.0 groupId=com.nike.cerberus artifactId=cms diff --git a/src/main/java/com/nike/cerberus/dao/AwsIamRoleDao.java b/src/main/java/com/nike/cerberus/dao/AwsIamRoleDao.java index c5f25014b..88354e21a 100644 --- a/src/main/java/com/nike/cerberus/dao/AwsIamRoleDao.java +++ b/src/main/java/com/nike/cerberus/dao/AwsIamRoleDao.java @@ -22,6 +22,7 @@ import com.nike.cerberus.record.AwsIamRoleRecord; import javax.inject.Inject; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; @@ -80,4 +81,20 @@ public int createIamRoleKmsKey(final AwsIamRoleKmsKeyRecord record) { public int updateIamRoleKmsKey(final AwsIamRoleKmsKeyRecord record) { return awsIamRoleMapper.updateIamRoleKmsKey(record); } + + public List getInactiveOrOrphanedKmsKeys(final OffsetDateTime keyInactiveDateTime) { + return awsIamRoleMapper.getInactiveOrOrphanedKmsKeys(keyInactiveDateTime); + } + + public List getOrphanedIamRoles() { + return awsIamRoleMapper.getOrphanedIamRoles(); + } + + public int deleteIamRoleById(final String id) { + return awsIamRoleMapper.deleteIamRoleById(id); + } + + public int deleteKmsKeyById(final String id) { + return awsIamRoleMapper.deleteKmsKeyById(id); + } } diff --git a/src/main/java/com/nike/cerberus/domain/CleanUpRequest.java b/src/main/java/com/nike/cerberus/domain/CleanUpRequest.java new file mode 100644 index 000000000..69cd28cd1 --- /dev/null +++ b/src/main/java/com/nike/cerberus/domain/CleanUpRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.domain; + +/** + * Clean up KMS key request. + */ +public class CleanUpRequest { + + private Integer kmsExpirationPeriodInDays; + + public Integer getKmsExpirationPeriodInDays() { + return kmsExpirationPeriodInDays; + } + + public CleanUpRequest setKmsExpirationPeriodInDays(Integer kmsExpirationPeriodInDays) { + this.kmsExpirationPeriodInDays = kmsExpirationPeriodInDays; + return this; + } +} diff --git a/src/main/java/com/nike/cerberus/domain/IamRolePermission.java b/src/main/java/com/nike/cerberus/domain/IamRolePermission.java index 8f053ab55..d9f8ea460 100644 --- a/src/main/java/com/nike/cerberus/domain/IamRolePermission.java +++ b/src/main/java/com/nike/cerberus/domain/IamRolePermission.java @@ -36,11 +36,9 @@ public class IamRolePermission { @Pattern(regexp = IAM_ROLE_ACCT_ID_REGEX, message = "IAM_ROLE_ACCT_ID_INVALID", groups = {Default.class, Updatable.class}) private String accountId; - // TODO: remove @Pattern(regexp = IAM_ROLE_NAME_REGEX, message = "IAM_ROLE_NAME_INVALID", groups = {Default.class, Updatable.class}) private String iamRoleName; - // TODO: remove @NotBlank(message = "IAM_ROLE_ROLE_ID_INVALID", groups = {Default.class, Updatable.class}) private String roleId; diff --git a/src/main/java/com/nike/cerberus/endpoints/admin/CleanUpInactiveOrOrphanedRecords.java b/src/main/java/com/nike/cerberus/endpoints/admin/CleanUpInactiveOrOrphanedRecords.java new file mode 100644 index 000000000..e08b72aac --- /dev/null +++ b/src/main/java/com/nike/cerberus/endpoints/admin/CleanUpInactiveOrOrphanedRecords.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.endpoints.admin; + +import com.google.inject.Inject; +import com.nike.cerberus.domain.CleanUpRequest; +import com.nike.cerberus.endpoints.AdminStandardEndpoint; +import com.nike.cerberus.security.VaultAuthPrincipal; +import com.nike.cerberus.service.CleanUpService; +import com.nike.riposte.server.http.RequestInfo; +import com.nike.riposte.server.http.ResponseInfo; +import com.nike.riposte.server.http.impl.FullResponseInfo; +import com.nike.riposte.util.AsyncNettyHelper; +import com.nike.riposte.util.Matcher; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.SecurityContext; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Cleans up inactive or orphaned KMS keys and IAM roles. + * + * This is required because orphaned CMKs, KMS key DB records, and IAM role DB records are created when an SDB is deleted. + * Thus this endpoint exists to clean up those already existing orphaned records in the DB, as well as orphaned records + * made from future SDB deletions. + * + * The reason that this clean up is not done at the time of SDB deletion is to lessen code complexity and give control + * to the administrators over when KMS keys are deleted. + */ +public class CleanUpInactiveOrOrphanedRecords extends AdminStandardEndpoint { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private static final int DEFAULT_KMS_KEY_INACTIVE_AFTER_N_DAYS = 30; + + private final CleanUpService cleanUpService; + + @Inject + public CleanUpInactiveOrOrphanedRecords(CleanUpService cleanUpService) { + this.cleanUpService = cleanUpService; + } + + @Override + public CompletableFuture> doExecute(final RequestInfo request, + final Executor longRunningTaskExecutor, + final ChannelHandlerContext ctx, + final SecurityContext securityContext) { + return CompletableFuture.supplyAsync( + AsyncNettyHelper.supplierWithTracingAndMdc(() -> cleanUp(request, securityContext), ctx), + longRunningTaskExecutor + ); + } + + private FullResponseInfo cleanUp(final RequestInfo request, + final SecurityContext securityContext) { + + final VaultAuthPrincipal vaultAuthPrincipal = (VaultAuthPrincipal) securityContext.getUserPrincipal(); + final String principal = vaultAuthPrincipal.getName(); + + log.info("Clean Up Event: the principal {} is attempting to clean up kms keys", principal); + + Integer expirationPeriodInDays = request.getContent().getKmsExpirationPeriodInDays(); + int kmsKeysInactiveAfterNDays = (expirationPeriodInDays == null) ? DEFAULT_KMS_KEY_INACTIVE_AFTER_N_DAYS : expirationPeriodInDays; + + cleanUpService.cleanUpInactiveAndOrphanedKmsKeys(kmsKeysInactiveAfterNDays); + cleanUpService.cleanUpOrphanedIamRoles(); + + return ResponseInfo.newBuilder() + .withHttpStatusCode(HttpResponseStatus.NO_CONTENT.code()) + .build(); + } + + @Override + public Matcher requestMatcher() { + return Matcher.match("/v1/cleanup", HttpMethod.PUT); + } + +} diff --git a/src/main/java/com/nike/cerberus/mapper/AwsIamRoleMapper.java b/src/main/java/com/nike/cerberus/mapper/AwsIamRoleMapper.java index c15fd88b9..62f93cb27 100644 --- a/src/main/java/com/nike/cerberus/mapper/AwsIamRoleMapper.java +++ b/src/main/java/com/nike/cerberus/mapper/AwsIamRoleMapper.java @@ -21,6 +21,7 @@ import com.nike.cerberus.record.AwsIamRoleRecord; import org.apache.ibatis.annotations.Param; +import java.time.OffsetDateTime; import java.util.List; /** @@ -51,4 +52,12 @@ int deleteIamRolePermission(@Param("safeDepositBoxId") String safeDepositBoxId, int deleteIamRolePermissions(@Param("safeDepositBoxId") String safeDepositBoxId); int updateIamRoleKmsKey(@Param("record") AwsIamRoleKmsKeyRecord record); + + List getInactiveOrOrphanedKmsKeys(@Param("keyInactiveDateTime") OffsetDateTime keyInactiveDateTime); + + List getOrphanedIamRoles(); + + int deleteIamRoleById(@Param("id") final String id); + + int deleteKmsKeyById(@Param("id") final String id); } diff --git a/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java index 5dcb7bcd6..bc5759daa 100644 --- a/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java +++ b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java @@ -22,6 +22,7 @@ import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.cerberus.config.CmsEnvPropertiesLoader; import com.nike.cerberus.endpoints.HealthCheckEndpoint; +import com.nike.cerberus.endpoints.admin.CleanUpInactiveOrOrphanedRecords; import com.nike.cerberus.endpoints.admin.GetSDBMetadata; import com.nike.cerberus.endpoints.admin.PutSDBMetadata; import com.nike.cerberus.endpoints.authentication.AuthenticateIamRole; @@ -184,7 +185,8 @@ public Set> appEndpoints( CreateSafeDepositBoxV1 createSafeDepositBoxV1, CreateSafeDepositBoxV2 createSafeDepositBoxV2, GetSDBMetadata getSDBMetadata, - PutSDBMetadata putSDBMetadata + PutSDBMetadata putSDBMetadata, + CleanUpInactiveOrOrphanedRecords cleanUpInactiveOrOrphanedRecords ) { return new LinkedHashSet<>(Arrays.>asList( healthCheckEndpoint, @@ -194,7 +196,7 @@ public Set> appEndpoints( getAllRoles, getRole, getSafeDepositBoxes, getSafeDepositBoxV1, getSafeDepositBoxV2, deleteSafeDepositBox, updateSafeDepositBoxV1, updateSafeDepositBoxV2, createSafeDepositBoxV1, createSafeDepositBoxV2, - getSDBMetadata, putSDBMetadata + getSDBMetadata, putSDBMetadata, cleanUpInactiveOrOrphanedRecords )); } diff --git a/src/main/java/com/nike/cerberus/service/CleanUpService.java b/src/main/java/com/nike/cerberus/service/CleanUpService.java new file mode 100644 index 000000000..5ec57afb5 --- /dev/null +++ b/src/main/java/com/nike/cerberus/service/CleanUpService.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.nike.cerberus.dao.AwsIamRoleDao; +import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord; +import com.nike.cerberus.record.AwsIamRoleRecord; +import com.nike.cerberus.util.DateTimeSupplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.nike.cerberus.service.KmsService.SOONEST_A_KMS_KEY_CAN_BE_DELETED; + +/** + * Service to clean up inactive and orphaned KMS keys + */ +@Singleton +public class CleanUpService { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final int DEFAULT_SLEEP_BETWEEN_KMS_CALLS = 10; // in seconds + + private final KmsService kmsService; + + private final AwsIamRoleDao awsIamRoleDao; + + private final DateTimeSupplier dateTimeSupplier; + + @Inject + public CleanUpService(KmsService kmsService, + AwsIamRoleDao awsIamRoleDao, + DateTimeSupplier dateTimeSupplier) { + this.kmsService = kmsService; + this.awsIamRoleDao = awsIamRoleDao; + this.dateTimeSupplier = dateTimeSupplier; + } + + /** + * Delete all AWS KMS keys and DB records for KMS keys that have not been used recently + * or are no longer associated with an SDB. + */ + public void cleanUpInactiveAndOrphanedKmsKeys(final int kmsKeysInactiveAfterNDays) { + + cleanUpInactiveAndOrphanedKmsKeys(kmsKeysInactiveAfterNDays, DEFAULT_SLEEP_BETWEEN_KMS_CALLS); + } + + /** + * Delete all AWS KMS keys and DB records for KMS keys that have not been used recently + * or are no longer associated with an SDB. + * @param kmsKeysInactiveAfterNDays - Consider KMS keys to be inactive after 'n' number of days + * @param sleepInSeconds - Sleep for 'n' seconds between AWS calls, to keep from exceeding the API limit + */ + protected void cleanUpInactiveAndOrphanedKmsKeys(final int kmsKeysInactiveAfterNDays, final int sleepInSeconds) { + + // get orphaned and inactive kms keys (not used in 'n' days) + final OffsetDateTime inactiveDateTime = dateTimeSupplier.get().minusDays(kmsKeysInactiveAfterNDays); + final List inactiveAndOrphanedKmsKeys = awsIamRoleDao.getInactiveOrOrphanedKmsKeys(inactiveDateTime); + + if (inactiveAndOrphanedKmsKeys.isEmpty()) { + logger.info("No keys to clean up."); + } else { + + // delete inactive and orphaned kms key records from DB + logger.info("Cleaning up orphaned or inactive KMS keys..."); + inactiveAndOrphanedKmsKeys.forEach(kmsKeyRecord -> { + try { + logger.info("Deleting orphaned or inactive KMS key: id={}, region={}, lastValidated={}", + kmsKeyRecord.getAwsKmsKeyId(), kmsKeyRecord.getAwsRegion(), kmsKeyRecord.getLastValidatedTs()); + + kmsService.scheduleKmsKeyDeletion(kmsKeyRecord.getAwsKmsKeyId(), kmsKeyRecord.getAwsRegion(), SOONEST_A_KMS_KEY_CAN_BE_DELETED); + awsIamRoleDao.deleteKmsKeyById(kmsKeyRecord.getId()); + + TimeUnit.SECONDS.sleep(sleepInSeconds); + } catch (InterruptedException ie) { + logger.error("Timeout between KMS key deletion was interrupted"); + Thread.currentThread().interrupt(); + } catch (Exception e) { + logger.error("There was a problem deleting KMS key with id: {}, region: {}", + kmsKeyRecord.getAwsIamRoleId(), + kmsKeyRecord.getAwsRegion()); + } + }); + } + } + + /** + * Delete all IAM role records that are no longer associated with an SDB. + */ + public void cleanUpOrphanedIamRoles() { + + // get orphaned iam role ids + final List orphanedIamRoleIds = awsIamRoleDao.getOrphanedIamRoles(); + + // delete orphaned iam role records from DB + orphanedIamRoleIds.forEach(awsIamRoleRecord -> { + try { + logger.info("Deleting orphaned IAM role: ARN={}, lastUpdated={}", + awsIamRoleRecord.getAwsIamRoleArn(), + awsIamRoleRecord.getLastUpdatedTs()); + awsIamRoleDao.deleteIamRoleById(awsIamRoleRecord.getId()); + } catch(Exception e) { + logger.error("There was a problem deleting orphaned IAM role with ARN: {}", + awsIamRoleRecord.getAwsIamRoleArn()); + } + }); + } +} + diff --git a/src/main/java/com/nike/cerberus/service/KmsService.java b/src/main/java/com/nike/cerberus/service/KmsService.java index 418475792..303a411d6 100644 --- a/src/main/java/com/nike/cerberus/service/KmsService.java +++ b/src/main/java/com/nike/cerberus/service/KmsService.java @@ -25,7 +25,9 @@ import com.amazonaws.services.kms.model.GetKeyPolicyResult; import com.amazonaws.services.kms.model.KeyMetadata; import com.amazonaws.services.kms.model.KeyUsageType; +import com.amazonaws.services.kms.model.NotFoundException; import com.amazonaws.services.kms.model.PutKeyPolicyRequest; +import com.amazonaws.services.kms.model.ScheduleKeyDeletionRequest; import com.google.inject.name.Named; import com.nike.backstopper.exception.ApiException; import com.nike.cerberus.aws.KmsClientFactory; @@ -55,9 +57,12 @@ public class KmsService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final String KMS_ALIAS_FORMAT = "alias/cerberus/%s"; - public static final String KMS_POLICY_VALIDATION_INTERVAL_OVERRIDE = "cms.kms.policy.validation.interval.millis.override"; - public static final Integer DEFAULT_KMS_VALIDATION_INTERVAL = 6000; // in milliseconds + private static final String KMS_POLICY_VALIDATION_INTERVAL_OVERRIDE = "cms.kms.policy.validation.interval.millis.override"; + + private static final Integer DEFAULT_KMS_VALIDATION_INTERVAL = 6000; // in milliseconds + + public static final Integer SOONEST_A_KMS_KEY_CAN_BE_DELETED = 7; // in days private final AwsIamRoleDao awsIamRoleDao; @@ -215,12 +220,32 @@ public void validatePolicy(AwsIamRoleKmsKeyRecord kmsKeyRecord, String iamPrinci // update last validated timestamp OffsetDateTime now = dateTimeSupplier.get(); updateKmsKey(kmsKeyRecord.getAwsIamRoleId(), kmsCMKRegion, SYSTEM_USER, now, now); + } catch(NotFoundException nfe) { + logger.warn("Failed to validate KMS policy because the KMS key did not exist, but the key record did." + + "Deleting the key record to prevent this from failing again: keyId: {} for IAM principal: {} in region: {}", + awsKmsKeyArn, iamPrincipalArn, kmsCMKRegion, nfe); + awsIamRoleDao.deleteKmsKeyById(kmsKeyRecord.getId()); } catch (AmazonServiceException e) { logger.warn(String.format("Failed to validate KMS policy for keyId: %s for IAM principal: %s in region: %s. API limit" + " may have been reached for validate call.", awsKmsKeyArn, iamPrincipalArn, kmsCMKRegion), e); } } + /** + * Delete a CMK in AWS + * @param kmsKeyId - The AWS KMS Key ID + * @param region - The KMS key region + */ + public void scheduleKmsKeyDeletion(String kmsKeyId, String region, Integer pendingWindowInDays) { + + final AWSKMSClient kmsClient = kmsClientFactory.getClient(region); + final ScheduleKeyDeletionRequest scheduleKeyDeletionRequest = new ScheduleKeyDeletionRequest() + .withKeyId(kmsKeyId) + .withPendingWindowInDays(pendingWindowInDays); + + kmsClient.scheduleKeyDeletion(scheduleKeyDeletionRequest); + } + /** * Determines if given KMS policy should be validated * @param kmsKeyRecord - KMS key record to check for validation diff --git a/src/main/resources/com/nike/cerberus/mapper/AwsIamRoleMapper.xml b/src/main/resources/com/nike/cerberus/mapper/AwsIamRoleMapper.xml index f0a8d5db2..a062a8abb 100644 --- a/src/main/resources/com/nike/cerberus/mapper/AwsIamRoleMapper.xml +++ b/src/main/resources/com/nike/cerberus/mapper/AwsIamRoleMapper.xml @@ -48,6 +48,20 @@ AWS_IAM_ROLE_ARN = #{awsIamRoleArn} + + + + INSERT INTO AWS_IAM_ROLE_KMS_KEY ( ID, @@ -192,4 +224,18 @@ SDBOX_ID = #{safeDepositBoxId} + + DELETE FROM + AWS_IAM_ROLE + WHERE + ID = #{id} + + + + DELETE FROM + AWS_IAM_ROLE_KMS_KEY + WHERE + ID = #{id} + + \ No newline at end of file diff --git a/src/test/java/com/nike/cerberus/dao/AwsIamRoleDaoTest.java b/src/test/java/com/nike/cerberus/dao/AwsIamRoleDaoTest.java index 78a150c68..087d17b0a 100644 --- a/src/test/java/com/nike/cerberus/dao/AwsIamRoleDaoTest.java +++ b/src/test/java/com/nike/cerberus/dao/AwsIamRoleDaoTest.java @@ -257,4 +257,24 @@ public void updateIamRoleKmsKey_returns_record_count() { assertThat(actualCount).isEqualTo(recordCount); } + + @Test + public void deleteIamRoleById_returns_record_count() { + final int recordCount = 1; + when(awsIamRoleMapper.deleteIamRoleById(iamRoleId)).thenReturn(recordCount); + + final int actualCount = subject.deleteIamRoleById(iamRoleId); + + assertThat(actualCount).isEqualTo(recordCount); + } + + @Test + public void deleteKmsKeyById_returns_record_count() { + final int recordCount = 1; + when(awsIamRoleMapper.deleteKmsKeyById(iamRoleKmsKeyId)).thenReturn(recordCount); + + final int actualCount = subject.deleteKmsKeyById(iamRoleKmsKeyId); + + assertThat(actualCount).isEqualTo(recordCount); + } } \ No newline at end of file diff --git a/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java b/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java index db869ca48..46cb2b233 100644 --- a/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java +++ b/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java @@ -20,7 +20,7 @@ public void test_pojo_structure_and_behavior() { List pojoClasses = PojoClassFactory.getPojoClasses("com.nike.cerberus.domain"); - Assert.assertEquals(18, pojoClasses.size()); + Assert.assertEquals(19, pojoClasses.size()); Validator validator = ValidatorBuilder.create() .with(new GetterMustExistRule()) diff --git a/src/test/java/com/nike/cerberus/service/CleanUpServiceTest.java b/src/test/java/com/nike/cerberus/service/CleanUpServiceTest.java new file mode 100644 index 000000000..b2f71727b --- /dev/null +++ b/src/test/java/com/nike/cerberus/service/CleanUpServiceTest.java @@ -0,0 +1,123 @@ +package com.nike.cerberus.service; + +import com.nike.cerberus.dao.AwsIamRoleDao; +import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord; +import com.nike.cerberus.record.AwsIamRoleRecord; +import com.nike.cerberus.util.DateTimeSupplier; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.time.OffsetDateTime; + +import static com.nike.cerberus.service.KmsService.SOONEST_A_KMS_KEY_CAN_BE_DELETED; +import static java.time.ZoneOffset.UTC; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +/** + * Tests the CleanUpService class + */ +public class CleanUpServiceTest { + + // class under test + private CleanUpService cleanUpService; + + // dependencies + @Mock + private KmsService kmsService; + + @Mock + private AwsIamRoleDao awsIamRoleDao; + + @Mock + private DateTimeSupplier dateTimeSupplier; + + private OffsetDateTime now = OffsetDateTime.now(UTC); + + @Before + public void setup() { + + initMocks(this); + + cleanUpService = new CleanUpService(kmsService, awsIamRoleDao, dateTimeSupplier); + } + + @Test + public void test_that_cleanUpInactiveAndOrphanedKmsKeys_succeeds() { + + int inactivePeriod = 30; + String keyRecordId = "key record id"; + String awsKeyId = "aws key id"; + String keyRegion = "key region"; + AwsIamRoleKmsKeyRecord keyRecord = mock(AwsIamRoleKmsKeyRecord.class); + when(keyRecord.getId()).thenReturn(keyRecordId); + when(keyRecord.getAwsKmsKeyId()).thenReturn(awsKeyId); + when(keyRecord.getAwsRegion()).thenReturn(keyRegion); + when(dateTimeSupplier.get()).thenReturn(now); + + OffsetDateTime inactiveCutoffDate = now.minusDays(inactivePeriod); + when(awsIamRoleDao.getInactiveOrOrphanedKmsKeys(inactiveCutoffDate)).thenReturn(Lists.newArrayList(keyRecord)); + + // perform the call + cleanUpService.cleanUpInactiveAndOrphanedKmsKeys(inactivePeriod, 0); + + verify(awsIamRoleDao).getInactiveOrOrphanedKmsKeys(inactiveCutoffDate); + verify(awsIamRoleDao).deleteKmsKeyById(keyRecordId); + verify(kmsService).scheduleKmsKeyDeletion(awsKeyId, keyRegion, SOONEST_A_KMS_KEY_CAN_BE_DELETED); + } + + @Test + public void test_that_cleanUpInactiveAndOrphanedKmsKeys_does_not_throw_exception_on_failure() { + + int inactivePeriod = 30; + String keyRecordId = "key record id"; + String awsKeyId = "aws key id"; + String keyRegion = "key region"; + AwsIamRoleKmsKeyRecord keyRecord = mock(AwsIamRoleKmsKeyRecord.class); + when(keyRecord.getId()).thenReturn(keyRecordId); + when(keyRecord.getAwsKmsKeyId()).thenReturn(awsKeyId); + when(keyRecord.getAwsRegion()).thenReturn(keyRegion); + when(dateTimeSupplier.get()).thenReturn(now); + + OffsetDateTime inactiveCutoffDate = now.minusDays(inactivePeriod); + when(awsIamRoleDao.getInactiveOrOrphanedKmsKeys(inactiveCutoffDate)).thenReturn(Lists.newArrayList(keyRecord)); + + when(awsIamRoleDao.deleteKmsKeyById(keyRecordId)).thenThrow(new NullPointerException()); + + cleanUpService.cleanUpInactiveAndOrphanedKmsKeys(inactivePeriod); + } + + @Test + public void test_that_cleanUpOrphanedIamRoles_succeeds() { + + String iamRoleRecordId = "iam role record id"; + AwsIamRoleRecord roleRecord = mock(AwsIamRoleRecord.class); + when(roleRecord.getId()).thenReturn(iamRoleRecordId); + + when(awsIamRoleDao.getOrphanedIamRoles()).thenReturn(Lists.newArrayList(roleRecord)); + + // perform the call + cleanUpService.cleanUpOrphanedIamRoles(); + + verify(awsIamRoleDao).getOrphanedIamRoles(); + verify(awsIamRoleDao).deleteIamRoleById(iamRoleRecordId); + } + + @Test + public void test_that_cleanUpOrphanedIamRoles_does_not_throw_exception_on_failure() { + + String iamRoleRecordId = "iam role record id"; + AwsIamRoleRecord roleRecord = mock(AwsIamRoleRecord.class); + when(roleRecord.getId()).thenReturn(iamRoleRecordId); + + when(awsIamRoleDao.getOrphanedIamRoles()).thenReturn(Lists.newArrayList(roleRecord)); + + when(awsIamRoleDao.deleteIamRoleById(iamRoleRecordId)).thenThrow(new NullPointerException()); + + cleanUpService.cleanUpOrphanedIamRoles(); + } +} \ No newline at end of file