Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ static String unsupportedResourceTypeMessage(String resourceType) {
public StackResource provisionStandalone(String resourceType, JsonNode properties, String region, String accountId) {
CloudFormationTemplateEngine engine = new CloudFormationTemplateEngine(
accountId, region, "cloudcontrol", "cloudcontrol",
Map.of(), new HashMap<>(), new HashMap<>(), Map.of(), Map.of(), objectMapper, name -> null);
Map.of(), new HashMap<>(), new HashMap<>(), Map.of(), Map.of(), objectMapper, name -> null,
value -> dynamicReferences.resolveGeneralPropertyDynamicReferences(value, region));
return provision("resource", resourceType, properties, engine, region, accountId, "cloudcontrol");
}

Expand Down Expand Up @@ -5723,7 +5724,10 @@ private String resolveOptional(JsonNode props, String name, CloudFormationTempla

/**
* Resolves CloudFormation dynamic references in a provisioned property value. Delegates to
* {@link CfnDynamicReferences}; the RDS master-credential paths are its only callers today.
* {@link CfnDynamicReferences}. {@code allowSsmSecure} is {@code true} only for the RDS
* master-credential properties resolved here directly; every other property value reaches
* {@link CfnDynamicReferences} through {@link CloudFormationTemplateEngine#resolveNode}, which
* disallows {@code ssm-secure} the same way the general path does.
*/
private String resolveDynamicReferences(String value, String region, boolean allowSsmSecure) {
return dynamicReferences.resolveDynamicReferences(value, region, allowSsmSecure);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.github.hectorvent.floci.services.cloudformation.model.StackEvent;
import io.github.hectorvent.floci.services.cloudformation.model.StackResource;
import io.github.hectorvent.floci.services.cloudformation.model.TemplateSummary;
import io.github.hectorvent.floci.services.cloudformation.provisioners.CfnDynamicReferences;
import io.github.hectorvent.floci.services.cloudformation.provisioners.CfnRollback;
import io.github.hectorvent.floci.services.cloudformation.provisioners.UpdateCleanupResult;
import io.github.hectorvent.floci.services.s3.S3Service;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class CloudFormationService implements ResourceProvider {
private final CloudFormationResourceProvisioner provisioner;
private final S3Service s3Service;
private final SsmService ssmService;
private final CfnDynamicReferences dynamicReferences;
private final ObjectMapper objectMapper;
private final EmulatorConfig config;
private final RegionResolver regionResolver;
Expand All @@ -81,12 +83,14 @@ public class CloudFormationService implements ResourceProvider {

@Inject
public CloudFormationService(CloudFormationResourceProvisioner provisioner, S3Service s3Service,
SsmService ssmService, ObjectMapper objectMapper, EmulatorConfig config,
SsmService ssmService, CfnDynamicReferences dynamicReferences,
ObjectMapper objectMapper, EmulatorConfig config,
RegionResolver regionResolver, Clock clock,
StorageFactory storageFactory) {
this.provisioner = provisioner;
this.s3Service = s3Service;
this.ssmService = ssmService;
this.dynamicReferences = dynamicReferences;
this.objectMapper = objectMapper;
this.config = config;
this.regionResolver = regionResolver;
Expand Down Expand Up @@ -1135,7 +1139,8 @@ private void executeTemplate(Stack stack, String templateBody, Map<String, Strin
CloudFormationTemplateEngine engine = new CloudFormationTemplateEngine(
accountId, region, stack.getStackName(),
stack.getStackId(), resolvedParams, physicalIds, resourceAttrs, conditions, mappings, objectMapper,
name -> exports.get(accountExportKey(accountId, exportKey(region, name))));
name -> exports.get(accountExportKey(accountId, exportKey(region, name))),
value -> dynamicReferences.resolveGeneralPropertyDynamicReferences(value, region));

StackResource resource = stack.getResources().get(logicalId);
StackResource previousResource = resource;
Expand Down Expand Up @@ -1233,7 +1238,8 @@ private void executeTemplate(Stack stack, String templateBody, Map<String, Strin
CloudFormationTemplateEngine finalEngine = new CloudFormationTemplateEngine(
accountId, region, stack.getStackName(),
stack.getStackId(), resolvedParams, physicalIds, resourceAttrs, conditions, mappings, objectMapper,
name -> exports.get(accountExportKey(accountId, exportKey(region, name))));
name -> exports.get(accountExportKey(accountId, exportKey(region, name))),
value -> dynamicReferences.resolveGeneralPropertyDynamicReferences(value, region));

// Resolve outputs before mutating stack/global export state, so failed updates do not
// leave stale or partially registered exports behind.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;

/**
Expand All @@ -36,6 +37,7 @@ public class CloudFormationTemplateEngine {
private final Map<String, JsonNode> mappings;
private final ObjectMapper objectMapper;
private final Function<String, String> importValueResolver;
private final UnaryOperator<String> dynamicReferenceResolver;

CloudFormationTemplateEngine(String accountId, String region, String stackName, String stackId,
Map<String, String> parameters,
Expand All @@ -45,6 +47,29 @@ public class CloudFormationTemplateEngine {
Map<String, JsonNode> mappings,
ObjectMapper objectMapper,
Function<String, String> importValueResolver) {
this(accountId, region, stackName, stackId, parameters, physicalIds, resourceAttributes,
conditions, mappings, objectMapper, importValueResolver, null);
}

/**
* @param dynamicReferenceResolver resolves a value that may contain CloudFormation dynamic
* references ({@code {{resolve:ssm:...}}},
* {@code {{resolve:secretsmanager:...}}}) against the live
* services, leaving a value with no dynamic reference syntax
* untouched. {@code null} skips this stage entirely, which
* keeps template values that only ever contain intrinsics
* (tests, Cloud Control desired-state resolution) free of the
* dependency.
*/
CloudFormationTemplateEngine(String accountId, String region, String stackName, String stackId,
Map<String, String> parameters,
Map<String, String> physicalIds,
Map<String, Map<String, String>> resourceAttributes,
Map<String, Boolean> conditions,
Map<String, JsonNode> mappings,
ObjectMapper objectMapper,
Function<String, String> importValueResolver,
UnaryOperator<String> dynamicReferenceResolver) {
this.accountId = accountId;
this.region = region;
this.stackName = stackName;
Expand All @@ -56,9 +81,21 @@ public class CloudFormationTemplateEngine {
this.mappings = mappings;
this.objectMapper = objectMapper;
this.importValueResolver = importValueResolver;
this.dynamicReferenceResolver = dynamicReferenceResolver;
}

/**
* Resolves a property value, including CloudFormation dynamic reference syntax
* ({@code {{resolve:ssm:...}}}, {@code {{resolve:secretsmanager:...}}}) in the result, the same
* way {@link #resolveNode} does. This is the entry point every scalar-property resolution in
* this codebase goes through, directly or via {@link #resolveNode}, so a dynamic reference is
* substituted regardless of which of the two a caller uses.
*/
public String resolve(JsonNode node) {
return resolveDynamicReferences(resolveIntrinsic(node)).asText();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 RDS Secure References Fail

RDS ssm-secure credentials now fail before reaching the RDS-specific resolver. resolveOptional invokes engine.resolve, whose new general resolver disallows ssm-secure. The later call with allowSsmSecure=true is therefore never reached, so valid MasterUserPassword references for DB instances and clusters raise ValidationError.

}

private String resolveIntrinsic(JsonNode node) {
if (node == null || node.isNull() || node.isMissingNode()) {
return "";
}
Expand Down Expand Up @@ -116,7 +153,10 @@ public JsonNode resolveNode(JsonNode node) {
if (node == null || node.isNull() || node.isMissingNode()) {
return node;
}
if (node.isTextual() || node.isNumber() || node.isBoolean()) {
if (node.isTextual()) {
return resolveDynamicReferences(node.textValue());
}
if (node.isNumber() || node.isBoolean()) {
return node;
}
if (node.isObject()) {
Expand Down Expand Up @@ -156,6 +196,22 @@ public JsonNode resolveNode(JsonNode node) {
return node;
}

/**
* Resolves a value against {@link #dynamicReferenceResolver} when it carries CloudFormation
* dynamic reference syntax ({@code {{resolve:ssm:...}}}, {@code {{resolve:secretsmanager:...}}}).
* CloudFormation substitutes these for any string property in a template, not only the RDS
* master-credential properties that first needed them (see
* <a href="https://github.com/floci-io/floci/issues/2213">#2213</a>), so {@link #resolveNode}
* applies this to every textual result: a literal string carrying the syntax outright, and the
* text an intrinsic function (e.g. {@code Fn::Sub}) produces.
*/
private TextNode resolveDynamicReferences(String value) {
if (dynamicReferenceResolver == null || value == null || !value.contains("{{resolve:")) {
return TextNode.valueOf(value);
}
return TextNode.valueOf(dynamicReferenceResolver.apply(value));
}

/**
* Resolves a node that must be stored as a JSON document (SNS/SQS RedrivePolicy and
* FilterPolicy, Step Functions definitions, IAM policy documents) to its string form.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ public String resolveDynamicReferences(String value, String region, boolean allo
return sb.toString();
}

/**
* Resolves dynamic references in a general (non-RDS-master-credential) template value, the
* same way {@link #resolveDynamicReferences} does, except a {@code ssm-secure} reference is
* left verbatim instead of rejected. {@code ssm-secure} is valid only for
* {@code MasterUsername}/{@code MasterUserPassword}, but this general stage runs on every
* property before the RDS provisioner gets its own turn at those two with
* {@code allowSsmSecure=true}; rejecting it here would reject a value RDS is about to accept.
*/
public String resolveGeneralPropertyDynamicReferences(String value, String region) {
if (value == null || !value.contains("{{resolve:")) {
return value;
}
Matcher m = DYNAMIC_REF.matcher(value);
StringBuilder sb = new StringBuilder();
int previousEnd = 0;
while (m.find()) {
rejectUnclosedDynamicReference(value.substring(previousEnd, m.start()));
String replacement = "ssm-secure".equals(m.group(1))
? m.group(0)
: resolveDynamicRef(m.group(1), m.group(2), region, false);
Comment on lines +87 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Secure References Bypass Validation

The general resolver leaves every ssm-secure reference unchanged so the RDS credential path can resolve it later. However, non-RDS properties have no later validation stage, so an unsupported {{resolve:ssm-secure:...}} value is provisioned as a literal placeholder instead of producing the AWS-compatible validation error required by the repository's AWS compatibility directive. Restrict this pass-through to RDS master credentials or reject unresolved secure references before provisioning other properties.

Context Used: AGENTS.md (source)

m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
previousEnd = m.end();
}
rejectUnclosedDynamicReference(value.substring(previousEnd));
m.appendTail(sb);
return sb.toString();
}

private String resolveDynamicRef(String service, String body, String region, boolean allowSsmSecure) {
if ("secretsmanager".equals(service)) {
// body = <secret-id-or-arn>:SecretString:<json-key>:<version-stage>:<version-id>. The
Expand Down
Loading
Loading