S3WebsiteTopologyConfiguration.java
package com.cloudforgeci.api.core.topology;
import com.cloudforgeci.api.core.SystemContext;
import com.cloudforge.core.enums.TopologyType;
import com.cloudforge.core.enums.SecurityProfile;
import com.cloudforgeci.api.interfaces.TopologyConfiguration;
import com.cloudforgeci.api.interfaces.Rule;
import software.amazon.awscdk.services.cloudfront.BehaviorOptions;
import software.amazon.awscdk.services.cloudfront.Distribution;
import software.amazon.awscdk.services.cloudfront.ViewerProtocolPolicy;
import software.amazon.awscdk.services.cloudfront.origins.S3Origin;
import software.amazon.awscdk.services.route53.ARecord;
import software.amazon.awscdk.services.route53.ARecordProps;
import software.amazon.awscdk.services.route53.AaaaRecord;
import software.amazon.awscdk.services.route53.AaaaRecordProps;
import software.amazon.awscdk.services.route53.RecordTarget;
import software.amazon.awscdk.services.route53.targets.CloudFrontTarget;
import software.amazon.awscdk.services.s3.BlockPublicAccess;
import software.amazon.awscdk.services.s3.Bucket;
import software.amazon.awscdk.services.s3.BucketAccessControl;
import software.amazon.awscdk.services.s3.BucketEncryption;
import software.amazon.awscdk.services.s3.BucketProps;
import java.util.ArrayList;
import java.util.List;
import static com.cloudforgeci.api.core.rules.RuleKit.*;
public final class S3WebsiteTopologyConfiguration implements TopologyConfiguration {
@Override public TopologyType kind() { return TopologyType.S3_WEBSITE; }
@Override public String id() { return "topology:S3_WEBSITE"; }
@Override
public List<Rule> rules(SystemContext c) {
var r = new ArrayList<Rule>();
boolean domainEnabled = c.cfc != null && c.cfc.domain() != null && !c.cfc.domain().isBlank();
boolean cfEnabled = c.cfc != null && Boolean.TRUE.equals(c.cfc.cloudfrontEnabled());
// S3 website topology does not provision Jenkins compute (runtime is irrelevant here).
// If TLS is enabled, we front with CloudFront (viewer TLS at edge).
r.add(ctx -> ctx.cfc.enableSsl() && !Boolean.TRUE.equals(ctx.cfc.cloudfrontEnabled())
? List.of("S3_WEBSITE with enableSsl = true requires cloudfront = true (viewer TLS at edge)") : List.of());
// If cloudfront + custom host expected, ensure fqdn (or can compute).
r.add(ctx -> {
if (!Boolean.TRUE.equals(ctx.cfc.cloudfrontEnabled())) return List.of();
boolean hasFqdn = ctx.cfc.fqdn() != null && !ctx.cfc.fqdn().isBlank();
boolean canCompute = ctx.cfc.subdomain() != null && ctx.cfc.domain() != null;
return (hasFqdn || canCompute) ? List.of() : List.of("cloudfront = true requires fqdn OR (subdomain + domain)");
});
r.addAll(List.of(
require("websiteBucket", x -> x.websiteBucket),
when(cfEnabled, require("distribution", x -> x.distribution)),
when(domainEnabled, require("hosted zone", x -> x.zone)),
when(domainEnabled && cfEnabled, require("certificate", x -> x.cert)),
forbid("asg", x -> x.asg),
forbid("fargate", x -> x.fargateService),
forbid("alb", x -> x.alb)
));
return r;
}
@Override
public void wire(SystemContext c) {
// 1) Website bucket (simple static hosting; fine to keep private and serve via CF+OAC later)
c.once("S3:WebsiteBucket", () -> {
if (c.websiteBucket.get().isPresent()) return;
// Determine encryption based on security profile
BucketEncryption encryption = determineEncryption(c);
Bucket bucket = new Bucket(c, "WebsiteBucket", BucketProps.builder()
.publicReadAccess(false) // prefer CF rather than public bucket
.accessControl(BucketAccessControl.PRIVATE)
.blockPublicAccess(BlockPublicAccess.BLOCK_ALL) // Security Hub compliance
.websiteIndexDocument("index.html")
.websiteErrorDocument("error.html")
.encryption(encryption)
.build());
c.websiteBucket.set(bucket);
});
// 2) CloudFront distribution (optional; required if enableSsl = true)
c.once("S3:Distribution", () -> {
if (!Boolean.TRUE.equals(c.cfc.cloudfrontEnabled())) return;
var origin = new S3Origin(c.websiteBucket.get().orElseThrow(
() -> new IllegalStateException("websiteBucket must exist before creating CloudFront Distribution")));
Distribution dist = Distribution.Builder.create(c, "WebsiteDist")
.defaultBehavior(BehaviorOptions.builder()
.origin(origin)
.viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
.build())
// If you’re passing a custom viewer cert via some upstream step, attach it through c.cert slot.
.certificate(c.cert.get().orElse(null))
.domainNames(resolveDomainNames(c))
.build();
c.distribution.set(dist);
});
// 3) Route53 alias to CF (ONLY if a zone is already set deterministically)
c.once("S3:CfAlias", () -> whenBoth(c.zone, c.distribution, (zone, dist) -> {
String rec = c.cfc.fqdn();
if (rec == null || rec.isBlank()) {
rec = c.cfc.subdomain() == null ? "" : c.cfc.subdomain();
}
var target = RecordTarget.fromAlias(new CloudFrontTarget(dist));
new ARecord(c, "CfAliasA", ARecordProps.builder()
.zone(zone).recordName(rec).target(target).build());
new AaaaRecord(c, "CfAliasAAAA", AaaaRecordProps.builder()
.zone(zone).recordName(rec).target(target).build());
}));
// (Optional) hook to add security headers / OAC policy if you also attach cert into c.cert
c.once("S3:Hardening", () -> whenAll(c.websiteBucket, c.distribution, c.cert, (bucket, dist, cert) -> {
// e.g., add response headers policy, S3 OAC, etc. Left empty intentionally for now.
}));
}
private static java.util.List<String> resolveDomainNames(SystemContext c) {
if (!Boolean.TRUE.equals(c.cfc.cloudfrontEnabled())) return java.util.List.of();
String fqdn = c.cfc.fqdn();
if (fqdn == null || fqdn.isBlank()) {
if (c.cfc.subdomain() != null && c.cfc.domain() != null) {
fqdn = c.cfc.subdomain().isBlank() ? c.cfc.domain() : c.cfc.subdomain() + "." + c.cfc.domain();
}
}
return (fqdn == null || fqdn.isBlank()) ? java.util.List.of() : java.util.List.of(fqdn);
}
/**
* Determine S3 bucket encryption based on security profile.
* - PRODUCTION: KMS encryption with AWS-managed keys (highest security)
* - STAGING: S3-managed encryption (AES-256)
* - DEV: S3-managed encryption (AES-256)
*/
private static BucketEncryption determineEncryption(SystemContext c) {
if (c.security == SecurityProfile.PRODUCTION) {
// Production: Use KMS encryption for enhanced security and audit capabilities
return BucketEncryption.KMS_MANAGED;
}
// Dev and Staging: Use S3-managed encryption (simpler, still encrypted at rest)
return BucketEncryption.S3_MANAGED;
}
}