From 548e7b27b91868fdf26bb47a8775838ac936fbb0 Mon Sep 17 00:00:00 2001 From: Mark Kuhn Date: Fri, 27 Jan 2023 16:11:20 -0800 Subject: [PATCH] update to subscriptionfilter handler (#103) --- .../inputs/bootstrap.yaml | 146 ------------------ .../inputs/inputs_1_create.json | 7 - .../inputs/inputs_1_invalid.json | 4 - .../subscriptionfilter/BaseHandlerStd.java | 75 ++++++++- .../subscriptionfilter/CreateHandler.java | 18 ++- .../logs/subscriptionfilter/ReadHandler.java | 28 ++-- .../logs/subscriptionfilter/Translator.java | 16 +- .../subscriptionfilter/AbstractTestBase.java | 7 +- .../BaseHandlerStdTest.java | 48 ++++++ .../subscriptionfilter/CreateHandlerTest.java | 7 +- .../subscriptionfilter/DeleteHandlerTest.java | 27 ++++ 11 files changed, 193 insertions(+), 190 deletions(-) delete mode 100644 aws-logs-subscriptionfilter/inputs/bootstrap.yaml delete mode 100644 aws-logs-subscriptionfilter/inputs/inputs_1_create.json delete mode 100644 aws-logs-subscriptionfilter/inputs/inputs_1_invalid.json create mode 100644 aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/BaseHandlerStdTest.java diff --git a/aws-logs-subscriptionfilter/inputs/bootstrap.yaml b/aws-logs-subscriptionfilter/inputs/bootstrap.yaml deleted file mode 100644 index 0750a2a..0000000 --- a/aws-logs-subscriptionfilter/inputs/bootstrap.yaml +++ /dev/null @@ -1,146 +0,0 @@ -Resources: - StackRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: - - cloudformation.amazonaws.com - Action: - - sts:AssumeRole - Path: / - Policies: - - PolicyName: StackRolePolicy - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - logs:DeleteSubscriptionFilter - - logs:DescribeSubscriptionFilters - - logs:PutSubscriptionFilter - Resource: "*" - - MyLogGroup: - Type: AWS::Logs::LogGroup - Properties: - RetentionInDays: 7 - - LambdaRole: - Type: AWS::IAM::Role - Properties: - RoleName: - Fn::Sub: boostrap-subscription-filter-lambda-role - AssumeRolePolicyDocument: - Statement: - - Action: - - sts:AssumeRole - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Version: 2012-10-17 - Path: / - - CloudWatchLogsDeliveryRole: - Type: AWS::IAM::Role - Properties: - Path: / - RoleName: CloudWatchDeliveryRole-Role - AssumeRolePolicyDocument: - Statement: - - Effect: Allow - Principal: - Service: - - logs.amazonaws.com - Action: - - 'sts:AssumeRole' - Policies: - - PolicyName: cloudwatch-delivery-policy - PolicyDocument: - Statement: - - Effect: Allow - Action: - - kinesis:PutRecord - - lambda:InvokeFunction - Resource: '*' - - LambdaFunction: - Type: AWS::Lambda::Function - Properties: - Runtime: nodejs14.x - Role: - Fn::GetAtt: - - LambdaRole - - Arn - Handler: index.handler - Code: - ZipFile: | - exports.handler = function(event, context) { - console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)) - response.send("Hello World") - } - - KinesisStream: - Type: AWS::Kinesis::Stream - Properties: - Name: MyKinesisStream - ShardCount: 2 - - KinesisStream2: - Type: AWS::Kinesis::Stream - Properties: - Name: MyKinesisStream2 - ShardCount: 3 - - -Outputs: - StackRoleArn: - Value: !GetAtt StackRole.Arn - Export: - Name: StackRoleArn - - LogGroupName: - Description: The name of the created LogGroup - Value: !Ref MyLogGroup - Export: - Name: LogGroupName - - LambdaDestinationArn: - Description: The ARN for the created Lambda function - Value: - Fn::GetAtt: - - LambdaFunction - - Arn - Export: - Name: LambdaDestinationArn - - KinesisStreamDestinationARN: - Description: The ARN for the created Kinesis stream - Value: - Fn::GetAtt: - - KinesisStream - - Arn - Export: - Name: KinesisStreamDestinationARN - - KinesisStreamDestinationARN2: - Description: The ARN for the second created Kinesis stream - Value: - Fn::GetAtt: - - KinesisStream2 - - Arn - Export: - Name: KinesisStreamDestinationARN2 - - RoleArn: - Description: The ARN of an IAM role that grants CloudWatch Logs permissions to deliver ingested log events to the destination stream - Value: - Fn::GetAtt: - - CloudWatchLogsDeliveryRole - - Arn - Export: - Name: RoleArn \ No newline at end of file diff --git a/aws-logs-subscriptionfilter/inputs/inputs_1_create.json b/aws-logs-subscriptionfilter/inputs/inputs_1_create.json deleted file mode 100644 index b41e6f8..0000000 --- a/aws-logs-subscriptionfilter/inputs/inputs_1_create.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "FilterName": "FilterName", - "DestinationArn": "{{KinesisStreamDestinationARN}}", - "LogGroupName": "{{LogGroupName}}", - "FilterPattern": "{$.userIdentity.type = Root}", - "RoleArn": "{{RoleArn}}" -} \ No newline at end of file diff --git a/aws-logs-subscriptionfilter/inputs/inputs_1_invalid.json b/aws-logs-subscriptionfilter/inputs/inputs_1_invalid.json deleted file mode 100644 index 718fbb1..0000000 --- a/aws-logs-subscriptionfilter/inputs/inputs_1_invalid.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "DestinationArn": "{{DestinationArn}}", - "LogGroupName": "{{LogGroupName}}" -} \ No newline at end of file diff --git a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/BaseHandlerStd.java b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/BaseHandlerStd.java index 871397e..e5c7678 100644 --- a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/BaseHandlerStd.java +++ b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/BaseHandlerStd.java @@ -3,10 +3,28 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.exception.AbortedException; import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; -import software.amazon.awssdk.services.cloudwatchlogs.model.*; +import software.amazon.awssdk.services.cloudwatchlogs.model.CloudWatchLogsException; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeSubscriptionFiltersRequest; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeSubscriptionFiltersResponse; +import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; +import software.amazon.awssdk.services.cloudwatchlogs.model.LimitExceededException; +import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceAlreadyExistsException; import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; -import software.amazon.cloudformation.exceptions.*; -import software.amazon.cloudformation.proxy.*; +import software.amazon.awssdk.services.cloudwatchlogs.model.ServiceUnavailableException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.CallChain; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import java.util.NoSuchElementException; @@ -67,7 +85,7 @@ protected boolean isAccessDeniedError(Exception e, final Logger logger) { e.getMessage(), e.getCause())); if (e instanceof CloudWatchLogsException) { - if (e.getMessage() != null && e.getMessage().contains("is not authorized to perform: logs:DescribeSubscriptionFilters")) { + if (e.getMessage() != null && e.getMessage().contains("is not authorized to perform: logs:")) { logger.log("AccessDenied exception in AccessDeniedCheck, passing"); return true; } @@ -90,12 +108,19 @@ protected void handleException(Exception e, Logger logger, final String stackId) logExceptionDetails(e, logger, stackId); if (e instanceof InvalidParameterException) { - throw new CfnInvalidRequestException(e); + throw new CfnInvalidRequestException(String.format("%s. %s", ResourceModel.TYPE_NAME, e.getMessage()), e); + } else if (e instanceof ResourceAlreadyExistsException) { + throw new CfnAlreadyExistsException(e); } else if (e instanceof ResourceNotFoundException) { throw new CfnNotFoundException(e); } else if (e instanceof ServiceUnavailableException) { throw new CfnServiceInternalErrorException(e); + } else if (e instanceof LimitExceededException) { + throw new CfnServiceLimitExceededException(e); + } else if (isAccessDeniedError(e, logger)) { + throw new CfnAccessDeniedException(e); } + throw new CfnGeneralServiceException(e); } protected HandlerErrorCode getExceptionDetails(final Exception e, final Logger logger, final String stackId) { @@ -133,4 +158,44 @@ protected boolean shouldThrowRetryException(final Exception e) { || e.getMessage().equals(ERROR_CODE_OPERATION_ABORTED_EXCEPTION); } + protected CallChain.Completed preCreateCheck( + final AmazonWebServicesClientProxy proxy, final CallbackContext callbackContext, + final ProxyClient proxyClient, final ResourceModel model) { + return proxy.initiate("AWS-Logs-SubscriptionFilter::Create::PreExistenceCheck", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall((awsRequest, sdkProxyClient) -> sdkProxyClient.injectCredentialsAndInvokeV2(awsRequest, + sdkProxyClient.client()::describeSubscriptionFilters)) + .handleError((request, exception, client, model1, context1) -> { + ProgressEvent progress; + if (exception instanceof InvalidParameterException) { + progress = ProgressEvent.failed(model, callbackContext, HandlerErrorCode.InvalidRequest, + exception.getMessage()); + } else if (exception instanceof ServiceUnavailableException) { + progress = ProgressEvent.failed(model, callbackContext, HandlerErrorCode.ServiceInternalError, + exception.getMessage()); + } else if (exception instanceof ResourceNotFoundException) { + progress = ProgressEvent.progress(model, callbackContext); + } else if (exception instanceof CloudWatchLogsException) { + progress = + ProgressEvent.failed(model, callbackContext, HandlerErrorCode.GeneralServiceException, + exception.getMessage()); + } else { + throw exception; + } + return progress; + }); + } + + protected boolean filterNameExists(final DescribeSubscriptionFiltersResponse response, ResourceModel model) { + if (response == null || response.subscriptionFilters() == null) { + return false; + } + if (!response.hasSubscriptionFilters()) { + return false; + } + if (response.subscriptionFilters().isEmpty()) { + return false; + } + return model.getFilterName().equals(response.subscriptionFilters().get(0).filterName()); + } } diff --git a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/CreateHandler.java b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/CreateHandler.java index 0412440..d5631e4 100644 --- a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/CreateHandler.java +++ b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/CreateHandler.java @@ -63,14 +63,24 @@ protected ProgressEvent handleRequest( logger.log(String.format("Filter name not present. Generated: %s as FilterName for stackID: %s", resourceIdentifier, request.getStackId())); } - return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + return ProgressEvent.progress(model, callbackContext) .then(progress -> + preCreateCheck(proxy, callbackContext, proxyClient, model).done(response -> { + if (filterNameExists(response, model)) { + return ProgressEvent.defaultFailureHandler( + new CfnAlreadyExistsException(ResourceModel.TYPE_NAME, model.getPrimaryIdentifier().toString()), + HandlerErrorCode.AlreadyExists + ); + } + return ProgressEvent.progress(model, callbackContext); + })) + .then(progress -> proxy.initiate(CALL_GRAPH_STRING, proxyClient, model, callbackContext) .translateToServiceRequest(Translator::translateToCreateRequest) - .makeServiceCall((putLifecycleHookRequest, client) -> client - .injectCredentialsAndInvokeV2(putLifecycleHookRequest, + .makeServiceCall((filterRequest, client) -> client + .injectCredentialsAndInvokeV2(filterRequest, client.client()::putSubscriptionFilter)) - .handleError((autoScalingRequest, e, proxyClient1, model1, context) -> { + .handleError((req, e, proxyClient1, model1, context) -> { // invalid parameter exception needs to be retried if (e instanceof AwsServiceException && ((AwsServiceException)e).awsErrorDetails() != null) { final AwsServiceException awsServiceException = (AwsServiceException) e; diff --git a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/ReadHandler.java b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/ReadHandler.java index 4b11c7b..a302237 100644 --- a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/ReadHandler.java +++ b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/ReadHandler.java @@ -1,6 +1,7 @@ package software.amazon.logs.subscriptionfilter; import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.cloudformation.exceptions.ResourceNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; @@ -28,21 +29,26 @@ protected ProgressEvent handleRequest( return proxy.initiate(CALL_GRAPH_STRING, proxyClient, model, callbackContext) .translateToServiceRequest(Translator::translateToReadRequest) - .makeServiceCall((cloudWatchLogsRequest, sdkProxyClient) -> { - return sdkProxyClient.injectCredentialsAndInvokeV2(cloudWatchLogsRequest, - sdkProxyClient.client()::describeSubscriptionFilters); - }).handleError((cloudWatchLogsRequest, e, _proxyClient, _model, ctx) -> { - if (isAccessDeniedError(e, logger)) { - return ProgressEvent.success(model, ctx); - } else { - final HandlerErrorCode handlerErrorCode = getExceptionDetails(e, logger, stackId); - return ProgressEvent.failed(model, callbackContext, handlerErrorCode, e.getMessage()); - } - }) + .makeServiceCall((cloudWatchLogsRequest, sdkProxyClient) -> sdkProxyClient.injectCredentialsAndInvokeV2(cloudWatchLogsRequest, + sdkProxyClient.client()::describeSubscriptionFilters)) + .handleError((cloudWatchLogsRequest, e, pc, md, ctx) -> handleError(e, model, ctx, stackId)) .done(awsResponse -> ProgressEvent.builder() .status(OperationStatus.SUCCESS) .resourceModel(Translator.translateFromReadResponse(awsResponse)) .build()); } + private ProgressEvent handleError( + final Exception e, + final ResourceModel model, + final CallbackContext callbackContext, + final String stackId) { + + if (isAccessDeniedError(e, logger) || e instanceof ResourceNotFoundException) { + return ProgressEvent.success(model, callbackContext); + } + + final HandlerErrorCode handlerErrorCode = getExceptionDetails(e, logger, stackId); + return ProgressEvent.failed(model, callbackContext, handlerErrorCode, e.getMessage()); + } } diff --git a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/Translator.java b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/Translator.java index 49334c2..befd399 100644 --- a/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/Translator.java +++ b/aws-logs-subscriptionfilter/src/main/java/software/amazon/logs/subscriptionfilter/Translator.java @@ -27,19 +27,15 @@ public class Translator { private static final int RESPONSE_LIMIT = 50; public static BaseHandlerException translateException(final AwsServiceException e) { - if (e instanceof LimitExceededException) { + if (e instanceof InvalidParameterException) { + return new CfnInvalidRequestException(String.format("%s. %s", ResourceModel.TYPE_NAME, e.getMessage()), e); + } else if (e instanceof LimitExceededException) { return new CfnServiceLimitExceededException(e); - } - if (e instanceof OperationAbortedException) { + } else if (e instanceof OperationAbortedException) { return new CfnResourceConflictException(e); - } - if (e instanceof InvalidParameterException) { - return new CfnInvalidRequestException(e); - } - else if (e instanceof ResourceNotFoundException) { + } else if (e instanceof ResourceNotFoundException) { return new CfnNotFoundException(e); - } - else if (e instanceof ServiceUnavailableException) { + } else if (e instanceof ServiceUnavailableException) { return new CfnServiceInternalErrorException(e); } return new CfnGeneralServiceException(e); diff --git a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/AbstractTestBase.java b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/AbstractTestBase.java index 9e64800..8d7afe0 100644 --- a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/AbstractTestBase.java +++ b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/AbstractTestBase.java @@ -53,12 +53,17 @@ public CloudWatchLogsClient client() { } static ResourceModel buildDefaultModel() { + return buildDefaultModel("filter-name"); + } + + static ResourceModel buildDefaultModel(String filterName) { return ResourceModel.builder() - .filterName("filter-name") + .filterName(filterName) .destinationArn("destination-arn") .filterPattern("[pattern]") .logGroupName("log-group-name") .roleArn("role-arn") .build(); + } } diff --git a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/BaseHandlerStdTest.java b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/BaseHandlerStdTest.java new file mode 100644 index 0000000..9f723ca --- /dev/null +++ b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/BaseHandlerStdTest.java @@ -0,0 +1,48 @@ +package software.amazon.logs.subscriptionfilter; + + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; +import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeSubscriptionFiltersResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BaseHandlerStdTest extends AbstractTestBase { + + BaseHandlerStd handler = new BaseHandlerStd() { + @Override + protected ProgressEvent handleRequest(AmazonWebServicesClientProxy proxy, ResourceHandlerRequest request, CallbackContext callbackContext, ProxyClient proxyClient, Logger logger) { + return null; + } + }; + + @ParameterizedTest + @MethodSource + void alreadyExists(String subscriptionNameToCreate, String returnedSubscriptionName, boolean shouldExist) { + final ResourceModel model = buildDefaultModel(returnedSubscriptionName); + final DescribeSubscriptionFiltersResponse response = DescribeSubscriptionFiltersResponse.builder() + .subscriptionFilters(Translator.translateToSDK(model)) + .build(); + + assertEquals(shouldExist, handler.filterNameExists(response, buildDefaultModel(subscriptionNameToCreate))); + } + + private static Stream alreadyExists() { + return Stream.of( + Arguments.of("subscription-name-suffix", "subscription-name", false), + Arguments.of("subscription-name", "subscription-name", true), + Arguments.of("subscription-name", "Subscription-Name", false), + Arguments.of("subscription-name", "subscription-name-suffix", false), + Arguments.of("subscription-name", "", false) + ); + } +} \ No newline at end of file diff --git a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/CreateHandlerTest.java b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/CreateHandlerTest.java index 3b7fe55..5f5a721 100644 --- a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/CreateHandlerTest.java +++ b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/CreateHandlerTest.java @@ -54,6 +54,7 @@ void handleRequest_Success() { .build(); when(proxyClient.client().describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class))) + .thenReturn(DescribeSubscriptionFiltersResponse.builder().build()) .thenReturn(describeResponse); when(proxyClient.client().putSubscriptionFilter(any(PutSubscriptionFilterRequest.class))) @@ -72,7 +73,7 @@ void handleRequest_Success() { assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); - verify(proxyClient.client(), times(1)).describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class)); + verify(proxyClient.client(), times(2)).describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class)); verify(proxyClient.client()).putSubscriptionFilter(any(PutSubscriptionFilterRequest.class)); } @@ -84,6 +85,7 @@ void handleRequest_Success2() { // return no existing Subscriptions for pre-create and then success response for create when(proxyClient.client().describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class))) + .thenReturn(DescribeSubscriptionFiltersResponse.builder().build()) .thenReturn(DescribeSubscriptionFiltersResponse.builder() .subscriptionFilters(Translator.translateToSDK(model)) .build()); @@ -104,7 +106,7 @@ void handleRequest_Success2() { assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); - verify(proxyClient.client(), times(1)).describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class)); + verify(proxyClient.client(), times(2)).describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class)); verify(proxyClient.client()).putSubscriptionFilter(any(PutSubscriptionFilterRequest.class)); } @@ -117,6 +119,7 @@ void handleRequest_Success_WithGeneratedName() { .build(); when(proxyClient.client().describeSubscriptionFilters(any(DescribeSubscriptionFiltersRequest.class))) + .thenReturn(DescribeSubscriptionFiltersResponse.builder().build()) .thenReturn(DescribeSubscriptionFiltersResponse.builder() .subscriptionFilters(Translator.translateToSDK(model)) .build()); diff --git a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/DeleteHandlerTest.java b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/DeleteHandlerTest.java index dc2e6ed..60d0c66 100644 --- a/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/DeleteHandlerTest.java +++ b/aws-logs-subscriptionfilter/src/test/java/software/amazon/logs/subscriptionfilter/DeleteHandlerTest.java @@ -2,11 +2,15 @@ import java.time.Duration; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteSubscriptionFilterRequest; import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteSubscriptionFilterResponse; import software.amazon.awssdk.services.cloudwatchlogs.model.InvalidParameterException; import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException; +import software.amazon.awssdk.services.cloudwatchlogs.model.CloudWatchLogsException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -111,4 +115,27 @@ void handleRequest_DeleteFailed() { assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) .isInstanceOf(CfnInvalidRequestException.class); } + + @Test + void handleRequest_AccessDenied() { + final ResourceModel model = buildDefaultModel(); + + final AwsErrorDetails accessDeniedDetails = AwsErrorDetails.builder(). + errorMessage("User: USER is not authorized to perform: logs:DeleteSubscriptionFilter on resource: " + + "LogGroupName: because no identity-based policy allows the logs:DeleteSubscriptionFilter action " + + "(Service: CloudWatchLogs, Status Code: 400, Request ID: 123)") + .build(); + + final AwsServiceException accessDeniedException = CloudWatchLogsException.builder().awsErrorDetails(accessDeniedDetails).build(); + + when(proxyClient.client().deleteSubscriptionFilter(ArgumentMatchers.any(DeleteSubscriptionFilterRequest.class))) + .thenThrow(accessDeniedException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)) + .isInstanceOf(CfnAccessDeniedException.class); + } }