Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/maven-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
with:
python-version: 3.13
- name: Install Python packages
run: pip install pre-commit cloudformation-cli cloudformation-cli-java-plugin setuptools
run: pip install pre-commit cloudformation-cli cloudformation-cli-java-plugin==2.1.1 setuptools
- name: Run pre-commit
run: pre-commit run --all-files
- name: Verify AWS::RDS::Test::Common
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public final class Commons {
SdkClientException.class)
.build();

public static final ErrorRuleSet ACCESS_DENIED_RULE_SET = ErrorRuleSet.extend(ErrorRuleSet.EMPTY_RULE_SET)
.withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.AccessDenied),
ErrorCode.AccessDenied,
ErrorCode.AccessDeniedException,
ErrorCode.NotAuthorized,
ErrorCode.UnauthorizedOperation)
.build();

private Commons() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package software.amazon.rds.common.util;

import com.amazonaws.arn.Arn;
import com.amazonaws.arn.ArnResource;
import com.amazonaws.util.StringUtils;


public final class ArnHelper {
private static String RDS_SERVICE = "rds";
public enum ResourceType {
DB_INSTANCE_SNAPSHOT("snapshot"),
DB_CLUSTER_SNAPSHOT("cluster-snapshot");

private String value;

ResourceType(String value) {
this.value = value;
}

public static ResourceType fromString(final String resourceString) {
if (!StringUtils.isNullOrEmpty(resourceString)) {
for (final ResourceType type : ResourceType.values()) {
if (type.value.equals(resourceString)) {
return type;
}
}
}
return null;
}
}

public static boolean isValidArn(final String arn) {
if (StringUtils.isNullOrEmpty(arn)) {
return false;
}
try {
Arn.fromString(arn);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}

public static String getRegionFromArn(final String arn) {
return Arn.fromString(arn).getRegion();
}

public static String getResourceNameFromArn(final String arn) {
return Arn.fromString(arn).getResource().getResource();
}

public static String getAccountIdFromArn(final String arn) {
return Arn.fromString(arn).getAccountId();
}

public static ResourceType getResourceType(String potentialArn) {
if (isValidArn(potentialArn)) {
final Arn arn = Arn.fromString(potentialArn);
if (!RDS_SERVICE.equalsIgnoreCase(arn.getService())) {
return null;
}
final ArnResource resource = arn.getResource();
return ResourceType.fromString(resource.getResourceType());
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package software.amazon.rds.common.validation;

@SuppressWarnings("serial")
public class ValidationAccessException extends Exception {
public ValidationAccessException(final String message) { super(message); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package software.amazon.rds.common.validation;

import com.google.common.collect.ImmutableMap;
import software.amazon.cloudformation.proxy.HandlerErrorCode;
import software.amazon.rds.common.error.ErrorStatus;
import software.amazon.rds.common.error.HandlerErrorStatus;
import software.amazon.rds.common.handler.Commons;
import software.amazon.rds.common.logging.RequestLogger;

import java.util.function.Supplier;

public class ValidationUtils {
private final static String MISSING_PERMISSION = "MissingPermission";
public static <T> T fetchResourceForValidation(Supplier<T> supplier, String requiredPermission) throws ValidationAccessException {
try {
return supplier.get();
} catch (Exception ex) {
final ErrorStatus error = Commons.ACCESS_DENIED_RULE_SET.handle(ex);
if (error instanceof HandlerErrorStatus &&
((HandlerErrorStatus)error).getHandlerErrorCode() == HandlerErrorCode.AccessDenied ){
throw new ValidationAccessException(requiredPermission);
}
throw ex;
}
}

public static void emitMetric(final RequestLogger logger, final String validationMetric, ValidationAccessException ex) {
logger.log(validationMetric, ImmutableMap.of(MISSING_PERMISSION, ex.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package software.amazon.rds.common.util;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class ArnHelperTests {
@Test
public void isValidArn_returnsFalseWhenNull() {
assertThat(ArnHelper.isValidArn(null)).isFalse();
}

@Test
public void isValidArn_returnsFalseWhenInvalid() {
assertThat(ArnHelper.isValidArn("invalid")).isFalse();
}

@Test
public void isValidArn_returnsTrueWhenValid() {
assertThat(ArnHelper.isValidArn("arn:aws:rds:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isTrue();
}

@Test
public void getResourceType_returnsClusterSnapshot() {
assertThat(ArnHelper.getResourceType("arn:aws:rds:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isEqualTo(ArnHelper.ResourceType.DB_CLUSTER_SNAPSHOT);
}

@Test
public void getResourceType_returnsNullForNonRdsService() {
assertThat(ArnHelper.getResourceType("arn:aws:someservice:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isNull();
}

@Test
public void getResourceType_returnsInstanceSnapshot() {
assertThat(ArnHelper.getResourceType("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo(ArnHelper.ResourceType.DB_INSTANCE_SNAPSHOT);
}

@Test
public void getRegionFromArn_returnsRegion() {
assertThat(ArnHelper.getRegionFromArn("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo("us-east-1");
}

@Test
public void getResourceName_returnsResourceName() {
assertThat(ArnHelper.getResourceNameFromArn("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo("mysnapshot");
}
}
1 change: 1 addition & 0 deletions aws-rds-dbcluster/aws-rds-dbcluster.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
"rds:ModifyDBCluster",
"rds:RestoreDBClusterFromSnapshot",
"rds:RestoreDBClusterToPointInTime",
"rds:DescribeDBClusterSnapshots",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource"
],
Expand Down
1 change: 1 addition & 0 deletions aws-rds-dbcluster/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Resources:
- "rds:CreateDBInstance"
- "rds:DeleteDBCluster"
- "rds:DeleteDBInstance"
- "rds:DescribeDBClusterSnapshots"
- "rds:DescribeDBClusters"
- "rds:DescribeDBSubnetGroups"
- "rds:DescribeEvents"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;
import org.apache.commons.collections.CollectionUtils;
Expand All @@ -27,6 +28,7 @@
import software.amazon.awssdk.services.rds.RdsClient;
import software.amazon.awssdk.services.rds.model.ClusterPendingModifiedValues;
import software.amazon.awssdk.services.rds.model.DBCluster;
import software.amazon.awssdk.services.rds.model.DBClusterSnapshot;
import software.amazon.awssdk.services.rds.model.DBSubnetGroup;
import software.amazon.awssdk.services.rds.model.DbClusterAlreadyExistsException;
import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException;
Expand All @@ -38,6 +40,7 @@
import software.amazon.awssdk.services.rds.model.DbInstanceNotFoundException;
import software.amazon.awssdk.services.rds.model.DbSubnetGroupDoesNotCoverEnoughAZsException;
import software.amazon.awssdk.services.rds.model.DbSubnetGroupNotFoundException;
import software.amazon.awssdk.services.rds.model.DescribeDbClusterSnapshotsResponse;
import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse;
import software.amazon.awssdk.services.rds.model.DescribeDbSubnetGroupsResponse;
import software.amazon.awssdk.services.rds.model.DescribeGlobalClustersResponse;
Expand All @@ -63,6 +66,7 @@
import software.amazon.awssdk.services.rds.model.StorageTypeNotSupportedException;
import software.amazon.awssdk.services.rds.model.Tag;
import software.amazon.awssdk.services.rds.model.WriteForwardingStatus;
import software.amazon.awssdk.services.rds.paginators.DescribeDBClustersIterable;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.exceptions.CfnNotStabilizedException;
Expand Down Expand Up @@ -90,6 +94,7 @@
import software.amazon.rds.common.request.RequestValidationException;
import software.amazon.rds.common.request.ValidatedRequest;
import software.amazon.rds.common.request.Validations;
import software.amazon.rds.common.util.ArnHelper;

public abstract class BaseHandlerStd extends BaseHandler<CallbackContext> {
public static final String RESOURCE_IDENTIFIER = "dbcluster";
Expand Down Expand Up @@ -297,6 +302,65 @@ protected DBSubnetGroup fetchDBSubnetGroup(
return response.dbSubnetGroups().get(0);
}

protected DBCluster fetchSourceDBCluster(
final String awsAccountId,
final ProxyClient<RdsClient> proxyClient,
final ResourceModel model
) {
// RDS API does not accept cross-account cluster identifiers when using --db-cluster-identifier option
// therefore resorting to this workaround
if (isCrossAccountSourceDBCluster(awsAccountId, model.getSourceDBClusterIdentifier())) {
final DescribeDBClustersIterable response = proxyClient.injectCredentialsAndInvokeIterableV2(
Translator.describeSourceDbClustersCrossAccountRequest(model),
proxyClient.client()::describeDBClustersPaginator
);

final List<DBCluster> matchingClusters = response.dbClusters()
.stream()
.filter(c -> model.getSourceDBClusterIdentifier().equalsIgnoreCase(Optional.ofNullable(c.dbClusterArn()).orElse("")))
.collect(Collectors.toList());
if (matchingClusters.isEmpty()) {
throw DbClusterNotFoundException.builder()
.message(String.format("SourceDbCluster %s doesn't refer to an existing DB cluster", model.getSourceDBClusterIdentifier()))
.build();
}
return matchingClusters.get(0);
}
final DescribeDbClustersResponse response = proxyClient.injectCredentialsAndInvokeV2(
Translator.describeSourceDbClustersRequest(model),
proxyClient.client()::describeDBClusters
);
if (response.dbClusters().isEmpty()) {
throw DbClusterNotFoundException.builder()
.message(String.format("SourceDbCluster %s doesn't refer to an existing DB cluster", model.getSourceDBClusterIdentifier()))
.build();
}
return response.dbClusters().get(0);
}

private boolean isCrossAccountSourceDBCluster(String awsCustomer, String sourceDBCluster) {
if (ArnHelper.isValidArn(sourceDBCluster)) {
return !StringUtils.equals(awsCustomer, ArnHelper.getAccountIdFromArn(sourceDBCluster));
}
return false;
}

protected DBClusterSnapshot fetchDBClusterSnapshot(
final ProxyClient<RdsClient> proxyClient,
final ResourceModel model
) {
final DescribeDbClusterSnapshotsResponse response = proxyClient.injectCredentialsAndInvokeV2(
Translator.describeDbClusterSnapshotRequest(model),
proxyClient.client()::describeDBClusterSnapshots
);
if (response.dbClusterSnapshots().isEmpty()) {
throw DbClusterSnapshotNotFoundException.builder()
.message(String.format("SnapshotIdentifier %s doesn't refer to an existing DB cluster snapshot", model.getSnapshotIdentifier()))
.build();
}
return response.dbClusterSnapshots().get(0);
}

protected SecurityGroup fetchSecurityGroup(
final ProxyClient<Ec2Client> ec2ProxyClient,
final String vpcId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.services.rds.model.ClusterScalabilityType;
import software.amazon.cloudformation.proxy.StdCallbackContext;
import software.amazon.rds.common.handler.ProbingContext;
import software.amazon.rds.common.handler.TaggingContext;
Expand All @@ -18,6 +19,7 @@ public class CallbackContext extends StdCallbackContext implements TaggingContex
private boolean modified;
private boolean rebooted;
private boolean deleting;
private ClusterScalabilityType clusterScalabilityType;

private Map<String, Long> timestamps;
private Map<String, Double> timeDelta;
Expand Down
Loading
Loading