package com.amazonaws.accessanalyzer.analyzer; import com.amazonaws.AmazonServiceException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Comparator; import java.util.Optional; import lombok.val; import software.amazon.awssdk.services.accessanalyzer.AccessAnalyzerClient; import software.amazon.awssdk.services.accessanalyzer.model.CreateArchiveRuleRequest; import software.amazon.awssdk.services.accessanalyzer.model.DeleteArchiveRuleRequest; import software.amazon.awssdk.services.accessanalyzer.model.ResourceNotFoundException; import software.amazon.awssdk.services.accessanalyzer.model.ServiceQuotaExceededException; import software.amazon.awssdk.services.accessanalyzer.model.TagResourceRequest; import software.amazon.awssdk.services.accessanalyzer.model.UntagResourceRequest; import software.amazon.awssdk.services.accessanalyzer.model.UpdateArchiveRuleRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; public class UpdateHandler extends BaseHandler<CallbackContext> { @Override public ProgressEvent<ResourceModel, CallbackContext> handleRequest( AmazonWebServicesClientProxy proxy, ResourceHandlerRequest<ResourceModel> request, CallbackContext callbackContext, Logger logger) { try (val client = ClientBuilder.getClient()) { return handleRequestWithClient(client, proxy, request, callbackContext, logger); } } @SuppressWarnings("WeakerAccess") @VisibleForTesting static ProgressEvent<ResourceModel, CallbackContext> handleRequestWithClient( AccessAnalyzerClient client, AmazonWebServicesClientProxy proxy, ResourceHandlerRequest<ResourceModel> request, @SuppressWarnings("unused") CallbackContext callbackContext, Logger logger) { val oldModel = request.getPreviousResourceState(); val newModel = request.getDesiredResourceState(); val arn = oldModel.getArn(); if (arn == null) { logger.log("Impossible: Null arn in current state of analyzer"); return ProgressEvent .failed(request.getDesiredResourceState(), null, HandlerErrorCode.InternalFailure, "Internal error"); } newModel.setArn(arn); // CFN only returns AnalyzerName used in the CREATE call // if it was part of the user template. // https://sage.amazon.com/questions/783984 val name = Optional.ofNullable(oldModel.getAnalyzerName()) .orElse(Util.arnToAnalyzerName(arn)); // AnalyzerName can't be changed, but if the user doesn't supply it use the existing name if (newModel.getAnalyzerName() == null) { logger.log("Setting new analyzer name to " + name); newModel.setAnalyzerName(name); } if (!name.equals(newModel.getAnalyzerName())) { return ProgressEvent .failed(request.getDesiredResourceState(), null, HandlerErrorCode.NotUpdatable, String .format("%s [%s] cannot be modified as AnalyzerName was changed", ResourceModel.TYPE_NAME, name)); } if (!oldModel.getType().equals(newModel.getType())) { return ProgressEvent .failed(request.getDesiredResourceState(), null, HandlerErrorCode.NotUpdatable, String .format("%s [%s] cannot be modified as Type was changed", ResourceModel.TYPE_NAME, name)); } // Tags val oldTags = Util.resourceTags(oldModel); val oldTagKeys = Util.setMap(oldTags, Tag::getKey); val newTags = Util.resourceTags(newModel); val newTagKeys = Util.setMap(newTags, Tag::getKey); val tagKeysToRemove = new ArrayList<String>(Sets.difference(oldTagKeys, newTagKeys)); tagKeysToRemove.sort(Comparator.naturalOrder()); // Stable order for testing val tagsToAdd = Util.filter(newTags, newTag -> oldTags.stream().noneMatch(newTag::equals)); tagsToAdd.sort(Comparator.comparing(Tag::getKey)); // Stable order for testing // Rules val oldRules = Util.resourceRules(oldModel); val oldRuleNames = Util.setMap(oldRules, Util::ruleName); val newRules = Util.resourceRules(newModel); val newRuleNames = Util.setMap(newRules, Util::ruleName); val ruleNamesToRemove = new ArrayList<String>(Sets.difference(oldRuleNames, newRuleNames)); ruleNamesToRemove.sort(Comparator.naturalOrder()); // Stable order for testing val rulesToAdd = new ArrayList<ArchiveRule>(); val rulesToUpdate = new ArrayList<ArchiveRule>(); for (val newRule : Util.resourceRules(newModel)) { if (oldRules.stream().noneMatch(newRule::equals)) { if (oldRuleNames.contains(Util.ruleName(newRule))) { rulesToUpdate.add(newRule); } else { rulesToAdd.add(newRule); } } } rulesToAdd.sort(Comparator.comparing(Util::ruleName)); // Stable order for testing rulesToUpdate.sort(Comparator.comparing(Util::ruleName)); // Stable order for testing try { if (!tagKeysToRemove.isEmpty()) { logger .log(String .format("Deleting %d tags for analyzer %s", tagKeysToRemove.size(), name)); val deleteTagsRequest = UntagResourceRequest.builder().resourceArn(arn) .tagKeys(tagKeysToRemove).build(); proxy.injectCredentialsAndInvokeV2(deleteTagsRequest, client::untagResource); } if (!tagsToAdd.isEmpty()) { logger.log(String.format("Adding %d tags for analyzer %s", tagsToAdd.size(), name)); val addTagsRequest = TagResourceRequest.builder().resourceArn(arn) .tags(Util.tagsToMap(tagsToAdd)).build(); proxy.injectCredentialsAndInvokeV2(addTagsRequest, client::tagResource); } ruleNamesToRemove.forEach(ruleName -> { logger.log(String.format("Deleting archive rule %s for analyzer %s", ruleName, name)); val deleteRuleRequest = DeleteArchiveRuleRequest.builder().analyzerName(name) .ruleName(ruleName).build(); proxy.injectCredentialsAndInvokeV2(deleteRuleRequest, client::deleteArchiveRule); }); rulesToAdd.forEach(rule -> { logger.log( String.format("Adding archive rule %s for analyzer %s", Util.ruleName(rule), name)); val inline = Util.inlineArchiveRuleFromArchiveRule(rule); val createRuleRequest = CreateArchiveRuleRequest.builder().analyzerName(name) .ruleName(inline.ruleName()).filter(inline.filter()).build(); proxy.injectCredentialsAndInvokeV2(createRuleRequest, client::createArchiveRule); }); rulesToUpdate.forEach(rule -> { logger.log( String.format("Updating archive rule %s for analyzer %s", Util.ruleName(rule), name)); val inline = Util.inlineArchiveRuleFromArchiveRule(rule); val updateRuleRequest = UpdateArchiveRuleRequest.builder().analyzerName(name) .ruleName(inline.ruleName()).filter(inline.filter()).build(); proxy.injectCredentialsAndInvokeV2(updateRuleRequest, client::updateArchiveRule); }); logger.log(String.format("%s [%s] Updated Successfully", ResourceModel.TYPE_NAME, name)); return ProgressEvent.defaultSuccessHandler(newModel); } catch (ResourceNotFoundException ex) { logger.log( String.format("%s [%s] not found and must be created", ResourceModel.TYPE_NAME, name)); return ProgressEvent .failed(request.getDesiredResourceState(), null, HandlerErrorCode.NotFound, String .format("%s [%s] not found and must be created", ResourceModel.TYPE_NAME, name)); } catch (ServiceQuotaExceededException ex) { logger.log( String.format("%s [%s] too many tags or archive rules", ResourceModel.TYPE_NAME, name)); return ProgressEvent.defaultFailureHandler(ex, HandlerErrorCode.ServiceLimitExceeded); } catch (AmazonServiceException ex) { if (ex.getStatusCode() == Util.SERVICE_VALIDATION_STATUS_CODE) { logger.log(String.format("%s [%s] Update Failed due to a service validation error", ResourceModel.TYPE_NAME, name)); return ProgressEvent.defaultFailureHandler(ex, HandlerErrorCode.InvalidRequest); } logger.log(String.format("%s [%s] Updated Failed", ResourceModel.TYPE_NAME, name)); return ProgressEvent.defaultFailureHandler(ex, HandlerErrorCode.ServiceInternalError); } catch (Exception ex) { logger.log(String.format("%s [%s] Updated Failed", ResourceModel.TYPE_NAME, name)); return ProgressEvent.defaultFailureHandler(ex, HandlerErrorCode.ServiceInternalError); } // TODO: Handle more exceptions } }