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

Commit

Permalink
Clean up inactive or orphaned records (#48)
Browse files Browse the repository at this point in the history
* Add admin endpoint to clean up KMS keys + IAM roles
  • Loading branch information
sdford authored Jun 23, 2017
1 parent 4c78ece commit 9263e90
Show file tree
Hide file tree
Showing 13 changed files with 509 additions and 8 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
# limitations under the License.
#

version=0.20.3
version=0.21.0
groupId=com.nike.cerberus
artifactId=cms
17 changes: 17 additions & 0 deletions src/main/java/com/nike/cerberus/dao/AwsIamRoleDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -80,4 +81,20 @@ public int createIamRoleKmsKey(final AwsIamRoleKmsKeyRecord record) {
public int updateIamRoleKmsKey(final AwsIamRoleKmsKeyRecord record) {
return awsIamRoleMapper.updateIamRoleKmsKey(record);
}

public List<AwsIamRoleKmsKeyRecord> getInactiveOrOrphanedKmsKeys(final OffsetDateTime keyInactiveDateTime) {
return awsIamRoleMapper.getInactiveOrOrphanedKmsKeys(keyInactiveDateTime);
}

public List<AwsIamRoleRecord> getOrphanedIamRoles() {
return awsIamRoleMapper.getOrphanedIamRoles();
}

public int deleteIamRoleById(final String id) {
return awsIamRoleMapper.deleteIamRoleById(id);
}

public int deleteKmsKeyById(final String id) {
return awsIamRoleMapper.deleteKmsKeyById(id);
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/nike/cerberus/domain/CleanUpRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 0 additions & 2 deletions src/main/java/com/nike/cerberus/domain/IamRolePermission.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<CleanUpRequest, Void> {

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<ResponseInfo<Void>> doExecute(final RequestInfo<CleanUpRequest> request,
final Executor longRunningTaskExecutor,
final ChannelHandlerContext ctx,
final SecurityContext securityContext) {
return CompletableFuture.supplyAsync(
AsyncNettyHelper.supplierWithTracingAndMdc(() -> cleanUp(request, securityContext), ctx),
longRunningTaskExecutor
);
}

private FullResponseInfo<Void> cleanUp(final RequestInfo<CleanUpRequest> 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.<Void>newBuilder()
.withHttpStatusCode(HttpResponseStatus.NO_CONTENT.code())
.build();
}

@Override
public Matcher requestMatcher() {
return Matcher.match("/v1/cleanup", HttpMethod.PUT);
}

}
9 changes: 9 additions & 0 deletions src/main/java/com/nike/cerberus/mapper/AwsIamRoleMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.nike.cerberus.record.AwsIamRoleRecord;
import org.apache.ibatis.annotations.Param;

import java.time.OffsetDateTime;
import java.util.List;

/**
Expand Down Expand Up @@ -51,4 +52,12 @@ int deleteIamRolePermission(@Param("safeDepositBoxId") String safeDepositBoxId,
int deleteIamRolePermissions(@Param("safeDepositBoxId") String safeDepositBoxId);

int updateIamRoleKmsKey(@Param("record") AwsIamRoleKmsKeyRecord record);

List<AwsIamRoleKmsKeyRecord> getInactiveOrOrphanedKmsKeys(@Param("keyInactiveDateTime") OffsetDateTime keyInactiveDateTime);

List<AwsIamRoleRecord> getOrphanedIamRoles();

int deleteIamRoleById(@Param("id") final String id);

int deleteKmsKeyById(@Param("id") final String id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -184,7 +185,8 @@ public Set<Endpoint<?>> appEndpoints(
CreateSafeDepositBoxV1 createSafeDepositBoxV1,
CreateSafeDepositBoxV2 createSafeDepositBoxV2,
GetSDBMetadata getSDBMetadata,
PutSDBMetadata putSDBMetadata
PutSDBMetadata putSDBMetadata,
CleanUpInactiveOrOrphanedRecords cleanUpInactiveOrOrphanedRecords
) {
return new LinkedHashSet<>(Arrays.<Endpoint<?>>asList(
healthCheckEndpoint,
Expand All @@ -194,7 +196,7 @@ public Set<Endpoint<?>> appEndpoints(
getAllRoles, getRole,
getSafeDepositBoxes, getSafeDepositBoxV1, getSafeDepositBoxV2,
deleteSafeDepositBox, updateSafeDepositBoxV1, updateSafeDepositBoxV2, createSafeDepositBoxV1, createSafeDepositBoxV2,
getSDBMetadata, putSDBMetadata
getSDBMetadata, putSDBMetadata, cleanUpInactiveOrOrphanedRecords
));
}

Expand Down
130 changes: 130 additions & 0 deletions src/main/java/com/nike/cerberus/service/CleanUpService.java
Original file line number Diff line number Diff line change
@@ -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<AwsIamRoleKmsKeyRecord> 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<AwsIamRoleRecord> 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());
}
});
}
}

Loading

0 comments on commit 9263e90

Please sign in to comment.