diff --git a/aws-codeguruprofiler-profilinggroup/README.md b/aws-codeguruprofiler-profilinggroup/README.md index a935845..b602982 100644 --- a/aws-codeguruprofiler-profilinggroup/README.md +++ b/aws-codeguruprofiler-profilinggroup/README.md @@ -51,7 +51,7 @@ pre-commit run --all-files && AWS_REGION=us-east-1 mvn clean verify package 5. Create a sample CloudFormation stack that defines a profiling group: ``` - aws cloudformation create-stack --region us-east-1 --template-body "file://sample-template.json" --stack-name "sample-profiling-group-resource-creation" + aws cloudformation create-stack --region us-east-1 --template-body "file://sample-template.json" --stack-name "sample-profiling-group-resource-creation" --capabilities CAPABILITY_IAM ``` 6. Validate the creation of the profiling group! diff --git a/aws-codeguruprofiler-profilinggroup/aws-codeguruprofiler-profilinggroup.json b/aws-codeguruprofiler-profilinggroup/aws-codeguruprofiler-profilinggroup.json index ea481ed..a54b67e 100644 --- a/aws-codeguruprofiler-profilinggroup/aws-codeguruprofiler-profilinggroup.json +++ b/aws-codeguruprofiler-profilinggroup/aws-codeguruprofiler-profilinggroup.json @@ -6,6 +6,10 @@ "Arn": { "type": "string", "pattern": "^arn:aws(-(cn|gov))?:[a-z-]+:(([a-z]+-)+[0-9]+):([0-9]{12}):[^.]+$" + }, + "ArnIam": { + "type": "string", + "pattern": "^arn:aws(-(cn|gov))?:iam::([0-9]{12}):[^.]+$" } }, "properties": { @@ -16,6 +20,23 @@ "maxLength": 255, "pattern": "^[\\w-]+$" }, + "AgentPermissions": { + "description": "The agent permissions attached to this profiling group.", + "type": "object", + "additionalProperties": false, + "required": [ + "Principals" + ], + "properties": { + "Principals": { + "description": "The principals for the agent permissions.", + "type": "array", + "items": { + "$ref": "#/definitions/ArnIam" + } + } + } + }, "Arn": { "description": "The Amazon Resource Name (ARN) of the specified profiling group.", "$ref": "#/definitions/Arn", @@ -40,7 +61,8 @@ "handlers": { "create": { "permissions": [ - "codeguru-profiler:CreateProfilingGroup" + "codeguru-profiler:CreateProfilingGroup", + "codeguru-profiler:PutPermission" ] }, "read": { diff --git a/aws-codeguruprofiler-profilinggroup/resource-role.yaml b/aws-codeguruprofiler-profilinggroup/resource-role.yaml index 76dd12c..43a2b10 100644 --- a/aws-codeguruprofiler-profilinggroup/resource-role.yaml +++ b/aws-codeguruprofiler-profilinggroup/resource-role.yaml @@ -27,6 +27,7 @@ Resources: - "codeguru-profiler:DeleteProfilingGroup" - "codeguru-profiler:DescribeProfilingGroup" - "codeguru-profiler:ListProfilingGroups" + - "codeguru-profiler:PutPermission" Resource: "*" Outputs: ExecutionRoleArn: diff --git a/aws-codeguruprofiler-profilinggroup/sample-template.json b/aws-codeguruprofiler-profilinggroup/sample-template.json index ec53a76..f89be0e 100644 --- a/aws-codeguruprofiler-profilinggroup/sample-template.json +++ b/aws-codeguruprofiler-profilinggroup/sample-template.json @@ -7,6 +7,43 @@ "Properties": { "ProfilingGroupName": "MySampleProfilingGroup" } + }, + "MyProfilingGroupWithAgentPermissions": { + "Type": "AWS::CodeGuruProfiler::ProfilingGroup", + "Properties": { + "ProfilingGroupName": "MySampleProfilingGroupWithAgentPermissions", + "AgentPermissions": { + "Principals": [ + { + "Fn::GetAtt": [ + "MyProfilingGroupRole", + "Arn" + ] + } + ] + } + } + }, + "MyProfilingGroupRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + } + } } }, "Outputs": { diff --git a/aws-codeguruprofiler-profilinggroup/src/main/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandler.java b/aws-codeguruprofiler-profilinggroup/src/main/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandler.java index 2e63eef..5c9e03d 100644 --- a/aws-codeguruprofiler-profilinggroup/src/main/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandler.java +++ b/aws-codeguruprofiler-profilinggroup/src/main/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandler.java @@ -1,9 +1,12 @@ package software.amazon.codeguruprofiler.profilinggroup; import software.amazon.awssdk.services.codeguruprofiler.CodeGuruProfilerClient; +import software.amazon.awssdk.services.codeguruprofiler.model.CodeGuruProfilerException; import software.amazon.awssdk.services.codeguruprofiler.model.ConflictException; import software.amazon.awssdk.services.codeguruprofiler.model.CreateProfilingGroupRequest; +import software.amazon.awssdk.services.codeguruprofiler.model.DeleteProfilingGroupRequest; import software.amazon.awssdk.services.codeguruprofiler.model.InternalServerException; +import software.amazon.awssdk.services.codeguruprofiler.model.PutPermissionRequest; import software.amazon.awssdk.services.codeguruprofiler.model.ServiceQuotaExceededException; import software.amazon.awssdk.services.codeguruprofiler.model.ThrottlingException; import software.amazon.awssdk.services.codeguruprofiler.model.ValidationException; @@ -17,6 +20,12 @@ import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import java.util.List; +import java.util.Optional; + +import static java.lang.String.format; +import static software.amazon.awssdk.services.codeguruprofiler.model.ActionGroup.AGENT_PERMISSIONS; + public class CreateHandler extends BaseHandler { private final CodeGuruProfilerClient profilerClient = CodeGuruProfilerClientBuilder.create(); @@ -28,21 +37,67 @@ public ProgressEvent handleRequest( final CallbackContext callbackContext, final Logger logger) { - final ResourceModel model = request.getDesiredResourceState(); final String awsAccountId = request.getAwsAccountId(); - final String idempotencyToken = request.getClientRequestToken(); - - try { - CreateProfilingGroupRequest createProfilingGroupRequest = CreateProfilingGroupRequest.builder() - .profilingGroupName(model.getProfilingGroupName()) - .clientToken(idempotencyToken) - .build(); + final ResourceModel model = request.getDesiredResourceState(); + final String pgName = model.getProfilingGroupName(); + CreateProfilingGroupRequest createProfilingGroupRequest = CreateProfilingGroupRequest.builder() + .profilingGroupName(pgName) + .clientToken(request.getClientRequestToken()) + .build(); + safelyInvokeApi(() -> { proxy.injectCredentialsAndInvokeV2(createProfilingGroupRequest, profilerClient::createProfilingGroup); + }); + logger.log(format("%s [%s] for accountId [%s] has been successfully created!", ResourceModel.TYPE_NAME, pgName, awsAccountId)); - logger.log(String.format("%s [%s] for accountId [%s] has been successfully created!", ResourceModel.TYPE_NAME, model.getProfilingGroupName(), awsAccountId)); + Optional> principals = principalsForAgentPermissionsFrom(model); + if (principals.isPresent()) { + putAgentPermissions(proxy, logger, pgName, principals.get(), awsAccountId); + logger.log(format("%s [%s] for accountId [%s] has been successfully updated with agent permissions!", + ResourceModel.TYPE_NAME, pgName, awsAccountId)); + } + + return ProgressEvent.defaultSuccessHandler(model); + } - return ProgressEvent.defaultSuccessHandler(model); + private void putAgentPermissions(final AmazonWebServicesClientProxy proxy, final Logger logger, + final String pgName, final List principals, final String awsAccountId) { + PutPermissionRequest putPermissionRequest = PutPermissionRequest.builder() + .profilingGroupName(pgName) + .actionGroup(AGENT_PERMISSIONS) + .principals(principals) + .build(); + + safelyInvokeApi(() -> { + try { + proxy.injectCredentialsAndInvokeV2(putPermissionRequest, profilerClient::putPermission); + } catch (CodeGuruProfilerException putPermissionException) { + logger.log(format("%s [%s] for accountId [%s] has failed when updating the agent permissions, trying to delete the profiling group!", + ResourceModel.TYPE_NAME, pgName, awsAccountId)); + deleteProfilingGroup(proxy, logger, pgName, awsAccountId, putPermissionException); + throw putPermissionException; + } + }); + } + + private void deleteProfilingGroup(AmazonWebServicesClientProxy proxy, Logger logger, + String pgName, String awsAccountId, CodeGuruProfilerException putPermissionException) { + DeleteProfilingGroupRequest deletePgRequest = DeleteProfilingGroupRequest.builder().profilingGroupName(pgName).build(); + try { + proxy.injectCredentialsAndInvokeV2(deletePgRequest, profilerClient::deleteProfilingGroup); + logger.log(format("%s [%s] for accountId [%s] has succeeded when deleting the profiling group!", + ResourceModel.TYPE_NAME, pgName, awsAccountId)); + } catch (CodeGuruProfilerException deleteException) { + logger.log(format("%s [%s] for accountId [%s] has failed when deleting the profiling group!", + ResourceModel.TYPE_NAME, pgName, awsAccountId)); + putPermissionException.addSuppressed(deleteException); + throw putPermissionException; + } + } + + private static void safelyInvokeApi(final Runnable lambda) { + try { + lambda.run(); } catch (ConflictException e) { throw new CfnAlreadyExistsException(e); } catch (InternalServerException e) { @@ -55,4 +110,14 @@ public ProgressEvent handleRequest( throw new CfnInvalidRequestException(ResourceModel.TYPE_NAME + e.getMessage(), e); } } + + private static Optional> principalsForAgentPermissionsFrom(final ResourceModel model) { + if (model.getAgentPermissions() == null) { + return Optional.empty(); + } + if (model.getAgentPermissions().getPrincipals() == null) { + return Optional.empty(); + } + return Optional.of(model.getAgentPermissions().getPrincipals()); + } } diff --git a/aws-codeguruprofiler-profilinggroup/src/test/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandlerTest.java b/aws-codeguruprofiler-profilinggroup/src/test/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandlerTest.java index e053374..a5a3f1f 100644 --- a/aws-codeguruprofiler-profilinggroup/src/test/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandlerTest.java +++ b/aws-codeguruprofiler-profilinggroup/src/test/java/software/amazon/codeguruprofiler/profilinggroup/CreateHandlerTest.java @@ -1,16 +1,21 @@ package software.amazon.codeguruprofiler.profilinggroup; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.codeguruprofiler.model.CodeGuruProfilerException; import software.amazon.awssdk.services.codeguruprofiler.model.ConflictException; import software.amazon.awssdk.services.codeguruprofiler.model.CreateProfilingGroupRequest; import software.amazon.awssdk.services.codeguruprofiler.model.CreateProfilingGroupResponse; +import software.amazon.awssdk.services.codeguruprofiler.model.DeleteProfilingGroupRequest; import software.amazon.awssdk.services.codeguruprofiler.model.InternalServerException; import software.amazon.awssdk.services.codeguruprofiler.model.ProfilingGroupDescription; +import software.amazon.awssdk.services.codeguruprofiler.model.PutPermissionRequest; import software.amazon.awssdk.services.codeguruprofiler.model.ServiceQuotaExceededException; import software.amazon.awssdk.services.codeguruprofiler.model.ThrottlingException; import software.amazon.awssdk.services.codeguruprofiler.model.ValidationException; @@ -25,101 +30,247 @@ import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import java.util.Arrays; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static software.amazon.codeguruprofiler.profilinggroup.RequestBuilder.makeInvalidRequest; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static software.amazon.awssdk.services.codeguruprofiler.model.ActionGroup.AGENT_PERMISSIONS; +import static software.amazon.codeguruprofiler.profilinggroup.RequestBuilder.makeRequest; import static software.amazon.codeguruprofiler.profilinggroup.RequestBuilder.makeValidRequest; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest { @Mock - private AmazonWebServicesClientProxy proxy; + private AmazonWebServicesClientProxy proxy = mock(AmazonWebServicesClientProxy.class); @Mock - private Logger logger; + private Logger logger = mock(Logger.class); + + private CreateHandler subject = new CreateHandler(); private ResourceHandlerRequest request; - @BeforeEach - public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); + private final String profilingGroupName = "Silver-2020"; + private final String clientToken = "clientTokenXXX"; + private final List principals = Arrays.asList("a", "bc"); - request = makeValidRequest(); - } + private final CreateProfilingGroupRequest createPgRequest = CreateProfilingGroupRequest.builder() + .profilingGroupName(profilingGroupName) + .clientToken(clientToken) + .build(); - @Test - public void testSuccessState() { - doReturn(CreateProfilingGroupResponse.builder() - .profilingGroup(ProfilingGroupDescription.builder() - .name("IronMan-Suit-34") - .build()) - .build()) - .when(proxy).injectCredentialsAndInvokeV2( - ArgumentMatchers.eq(CreateProfilingGroupRequest - .builder() - .profilingGroupName("IronMan-Suit-34").clientToken("clientTokenXXX") - .build()), any()); - - final ProgressEvent response - = new CreateHandler().handleRequest(proxy, request, null, logger); + private final PutPermissionRequest putPermissionsRequest = PutPermissionRequest.builder() + .profilingGroupName(profilingGroupName) + .actionGroup(AGENT_PERMISSIONS) + .principals(principals) + .build(); - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } + private final DeleteProfilingGroupRequest deleteProfilingGroupRequest = DeleteProfilingGroupRequest.builder() + .profilingGroupName(profilingGroupName) + .build(); - @Test - public void testConflictException() { - doThrow(ConflictException.builder().build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + @Nested + class WhenPermissionsAreNotSet { - assertThrows(CfnAlreadyExistsException.class, () -> - new CreateHandler().handleRequest(proxy, request, null, logger)); - } + @BeforeEach + public void setup() { + request = makeRequest(ResourceModel.builder().profilingGroupName(profilingGroupName).build()); - @Test - public void testInternalServerException() { - doThrow(InternalServerException.builder().build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + doReturn(CreateProfilingGroupResponse.builder() + .profilingGroup(ProfilingGroupDescription.builder() + .name(profilingGroupName) + .build()) + .build()) + .when(proxy).injectCredentialsAndInvokeV2(eq(createPgRequest), any()); + } - assertThrows(CfnServiceInternalErrorException.class, () -> - new CreateHandler().handleRequest(proxy, request, null, logger)); + @Test + public void testSuccess() { + final ProgressEvent response = subject.handleRequest(proxy, request, null, logger); + + assertSuccessfulResponse(response); + } + + @Test + public void testCorrectOperationIsCalled() { + subject.handleRequest(proxy, request, null, logger); + + verify(proxy, times(1)).injectCredentialsAndInvokeV2(eq(createPgRequest), any()); + verifyNoMoreInteractions(proxy); + } } - @Test - public void testServiceQuotaExceededException() { - doThrow(ServiceQuotaExceededException.builder().build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + @Nested + class WhenPermissionsAreSet { + + @BeforeEach + public void setup() { + doReturn(CreateProfilingGroupResponse.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(eq(createPgRequest), any()); + } + + @Nested + class WhenPrincipalsAreNotSet { + + @Test + public void testNullPermissions() { + ResourceModel model = newResourceModel(null); + request = makeRequest(model); + assertSuccessfulResponse(subject.handleRequest(proxy, request, null, logger)); + } + + @Test + public void testNullPrincipals() { + ResourceModel model = newResourceModel(AgentPermissions.builder().principals(null).build()); + request = makeRequest(model); + assertSuccessfulResponse(subject.handleRequest(proxy, request, null, logger)); + } + + @AfterEach + public void testCorrectOperationIsCalled() { + verify(proxy, times(1)).injectCredentialsAndInvokeV2(eq(createPgRequest), any()); + verifyNoMoreInteractions(proxy); + } + } + + @Nested + class WhenPrincipalsAreSet { + + @BeforeEach + public void setup() { + ResourceModel model = newResourceModel(AgentPermissions.builder().principals(principals).build()); + request = makeRequest(model); + } + + @Test + public void testSuccess() { + assertSuccessfulResponse(subject.handleRequest(proxy, request, null, logger)); + verify(proxy, times(1)).injectCredentialsAndInvokeV2(eq(createPgRequest), any()); + verify(proxy, times(1)).injectCredentialsAndInvokeV2(eq(putPermissionsRequest), any()); + verifyNoMoreInteractions(proxy); + } + + @Test + public void testPutPermissionsFailsAssertExceptionType() { + doThrow(ConflictException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(eq(putPermissionsRequest), any()); - assertThrows(CfnServiceLimitExceededException.class, () -> - new CreateHandler().handleRequest(proxy, request, null, logger)); + CfnAlreadyExistsException exception = assertThrows(CfnAlreadyExistsException.class, + () -> subject.handleRequest(proxy, request, null, logger)); + + assertThat(exception).hasCauseExactlyInstanceOf(ConflictException.class); + assertThat(exception).hasNoSuppressedExceptions(); + } + + @Test + public void testPutPermissionsFailsAndDeleteProfilingGroupFailsAssertExceptionType() { + Throwable deleteException = InternalServerException.builder().build(); + doThrow(ConflictException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(eq(putPermissionsRequest), any()); + doThrow(deleteException) + .when(proxy).injectCredentialsAndInvokeV2(eq(deleteProfilingGroupRequest), any()); + + CfnAlreadyExistsException exception = assertThrows(CfnAlreadyExistsException.class, + () -> subject.handleRequest(proxy, request, null, logger)); + + assertThat(exception).hasCauseExactlyInstanceOf(ConflictException.class); + assertThat(exception.getCause()).hasSuppressedException(deleteException); + } + } } - @Test - public void testThrottlingException() { - doThrow(ThrottlingException.builder().build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + @Nested + class WhenThereIsAnException { + + @BeforeEach + public void setup() { + request = makeValidRequest(); + } + + @Test + public void testConflictException() { + doThrow(ConflictException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + CfnAlreadyExistsException exception = assertThrows(CfnAlreadyExistsException.class, () -> subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasCauseExactlyInstanceOf(ConflictException.class); + } + + @Test + public void testServiceQuotaExceededException() { + doThrow(ServiceQuotaExceededException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + CfnServiceLimitExceededException exception = assertThrows(CfnServiceLimitExceededException.class, () -> + subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasCauseExactlyInstanceOf(ServiceQuotaExceededException.class); + } + + @Test + public void testInternalServerException() { + doThrow(InternalServerException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + CfnServiceInternalErrorException exception = assertThrows(CfnServiceInternalErrorException.class, () -> + subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasCauseExactlyInstanceOf(InternalServerException.class); + } + + @Test + public void testThrottlingException() { + doThrow(ThrottlingException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); - assertThrows(CfnThrottlingException.class, () -> - new CreateHandler().handleRequest(proxy, request, null, logger)); + CfnThrottlingException exception = assertThrows(CfnThrottlingException.class, () -> + subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasCauseExactlyInstanceOf(ThrottlingException.class); + } + + @Test + public void testValidationException() { + doThrow(ValidationException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + CfnInvalidRequestException exception = assertThrows(CfnInvalidRequestException.class, () -> + subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasCauseExactlyInstanceOf(ValidationException.class); + } + + @Test + public void testAnyOtherCodeGuruExceptionException() { + doThrow(CodeGuruProfilerException.builder().build()) + .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + + CodeGuruProfilerException exception = assertThrows(CodeGuruProfilerException.class, () -> + subject.handleRequest(proxy, request, null, logger)); + assertThat(exception).hasNoCause(); + } } - @Test - public void testValidationException() { - doThrow(ValidationException.builder().build()) - .when(proxy).injectCredentialsAndInvokeV2(any(), any()); + private ResourceModel newResourceModel(final AgentPermissions permissions) { + return ResourceModel.builder() + .profilingGroupName(profilingGroupName) + .agentPermissions(permissions) + .build(); + } - assertThrows(CfnInvalidRequestException.class, () -> - new CreateHandler().handleRequest(proxy, makeInvalidRequest(), null, logger)); + private void assertSuccessfulResponse(ProgressEvent response) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); } }