Skip to content

Commit d4276b0

Browse files
authored
Merge pull request #1568 from aligent/feature/SMGO-1185_cors_acesss_control-origins
SMG-1185: Added CORS and NoIndexNoFollow Response Header Policies
2 parents 580a689 + ef99196 commit d4276b0

File tree

4 files changed

+454
-2
lines changed

4 files changed

+454
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@aligent/cdk-static-hosting": minor
3+
---
4+
5+
Added optional CORS Response Header Policy and added NoIndexNoFollow Response Header Policy

packages/static-hosting/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import {
22
StaticHosting,
33
StaticHostingProps,
44
remapPath,
5+
ResponseHeaderMappings,
56
} from "./lib/static-hosting";
67
import { CSP } from "./types/csp";
78

8-
export { StaticHosting, StaticHostingProps, CSP, remapPath };
9+
export {
10+
StaticHosting,
11+
StaticHostingProps,
12+
CSP,
13+
remapPath,
14+
ResponseHeaderMappings,
15+
};

packages/static-hosting/lib/static-hosting.test.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,332 @@ describe("StaticHosting", () => {
521521
},
522522
});
523523
});
524+
});
525+
526+
describe("CORS Configuration", () => {
527+
it("should not create CORS policy when corsConfig is not provided", () => {
528+
const { stack } = createTestStack();
529+
const hosting = new StaticHosting(stack, "TestConstruct", defaultProps);
530+
531+
expect(hosting.corsResponseHeadersPolicy).toBeUndefined();
532+
});
533+
534+
it("should create CORS policy with defaults when corsConfig is provided", () => {
535+
const { stack } = createTestStack();
536+
const hosting = new StaticHosting(stack, "TestConstruct", {
537+
...defaultProps,
538+
corsConfig: {
539+
accessControlAllowOrigins: [
540+
"https://example.com",
541+
"https://app.example.com",
542+
],
543+
},
544+
});
545+
546+
expect(hosting.corsResponseHeadersPolicy).toBeDefined();
547+
548+
const template = Template.fromStack(stack);
549+
template.hasResourceProperties("AWS::CloudFront::ResponseHeadersPolicy", {
550+
ResponseHeadersPolicyConfig: {
551+
CorsConfig: {
552+
AccessControlAllowCredentials: false,
553+
AccessControlAllowHeaders: {
554+
Items: ["*"],
555+
},
556+
AccessControlAllowMethods: {
557+
Items: ["GET", "HEAD", "OPTIONS"],
558+
},
559+
AccessControlAllowOrigins: {
560+
Items: ["https://example.com", "https://app.example.com"],
561+
},
562+
OriginOverride: true,
563+
},
564+
},
565+
});
566+
});
567+
568+
it("should create CORS policy with custom settings when provided", () => {
569+
const { stack } = createTestStack();
570+
new StaticHosting(stack, "TestConstruct", {
571+
...defaultProps,
572+
corsConfig: {
573+
accessControlAllowOrigins: ["https://example.com"],
574+
accessControlAllowCredentials: true,
575+
accessControlAllowHeaders: ["Content-Type", "Authorization"],
576+
accessControlAllowMethods: ["GET", "HEAD", "OPTIONS", "POST"],
577+
originOverride: false,
578+
},
579+
});
580+
581+
const template = Template.fromStack(stack);
582+
template.hasResourceProperties("AWS::CloudFront::ResponseHeadersPolicy", {
583+
ResponseHeadersPolicyConfig: {
584+
CorsConfig: {
585+
AccessControlAllowCredentials: true,
586+
AccessControlAllowHeaders: {
587+
Items: ["Content-Type", "Authorization"],
588+
},
589+
AccessControlAllowMethods: {
590+
Items: ["GET", "HEAD", "OPTIONS", "POST"],
591+
},
592+
AccessControlAllowOrigins: {
593+
Items: ["https://example.com"],
594+
},
595+
OriginOverride: false,
596+
},
597+
},
598+
});
599+
});
600+
601+
it("should apply CORS policy to static file behaviors", () => {
602+
const { stack } = createTestStack();
603+
new StaticHosting(stack, "TestConstruct", {
604+
...defaultProps,
605+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
606+
});
607+
608+
const template = Template.fromStack(stack);
609+
const distribution = template.findResources(
610+
"AWS::CloudFront::Distribution"
611+
);
612+
const distConfig =
613+
Object.values(distribution)[0].Properties.DistributionConfig;
614+
615+
// Check that static file behaviors have a response headers policy
616+
const jsBehavior = distConfig.CacheBehaviors.find(
617+
(b: { PathPattern: string }) => b.PathPattern === "*.js"
618+
);
619+
expect(jsBehavior).toBeDefined();
620+
expect(jsBehavior.ResponseHeadersPolicyId).toBeDefined();
621+
622+
const cssBehavior = distConfig.CacheBehaviors.find(
623+
(b: { PathPattern: string }) => b.PathPattern === "*.css"
624+
);
625+
expect(cssBehavior).toBeDefined();
626+
expect(cssBehavior.ResponseHeadersPolicyId).toBeDefined();
627+
});
628+
629+
it("should not apply CORS policy to static files when accessControlAllowOrigins is empty array", () => {
630+
const { stack } = createTestStack();
631+
const hosting = new StaticHosting(stack, "TestConstruct", {
632+
...defaultProps,
633+
corsConfig: { accessControlAllowOrigins: [] },
634+
});
635+
636+
expect(hosting.corsResponseHeadersPolicy).toBeUndefined();
637+
638+
const template = Template.fromStack(stack);
639+
const distribution = template.findResources(
640+
"AWS::CloudFront::Distribution"
641+
);
642+
const distConfig =
643+
Object.values(distribution)[0].Properties.DistributionConfig;
644+
645+
// Check that static file behaviors do not have a response headers policy
646+
const jsBehavior = distConfig.CacheBehaviors.find(
647+
(b: { PathPattern: string }) => b.PathPattern === "*.js"
648+
);
649+
expect(jsBehavior).toBeDefined();
650+
expect(jsBehavior.ResponseHeadersPolicyId).toBeUndefined();
651+
});
652+
653+
it("should expose corsResponseHeadersPolicy for downstream use", () => {
654+
const { stack } = createTestStack();
655+
const hosting = new StaticHosting(stack, "TestConstruct", {
656+
...defaultProps,
657+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
658+
});
659+
660+
// The policy should be accessible for downstream projects to use
661+
// in their own custom behaviors
662+
expect(hosting.corsResponseHeadersPolicy).toBeDefined();
663+
expect(typeof hosting.corsResponseHeadersPolicy).toBe("object");
664+
});
665+
666+
it("should apply CORS policy to remapPaths behaviors", () => {
667+
const { stack } = createTestStack();
668+
new StaticHosting(stack, "TestConstruct", {
669+
...defaultProps,
670+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
671+
remapPaths: [{ from: "/test-path", to: "/remapped-path" }],
672+
});
673+
674+
const template = Template.fromStack(stack);
675+
const distribution = template.findResources(
676+
"AWS::CloudFront::Distribution"
677+
);
678+
const distConfig =
679+
Object.values(distribution)[0].Properties.DistributionConfig;
680+
681+
const remapBehavior = distConfig.CacheBehaviors.find(
682+
(b: { PathPattern: string }) => b.PathPattern === "/test-path"
683+
);
684+
expect(remapBehavior).toBeDefined();
685+
expect(remapBehavior.ResponseHeadersPolicyId).toBeDefined();
686+
});
687+
688+
it("should apply CORS policy to remapBackendPaths behaviors", () => {
689+
const { stack } = createTestStack();
690+
new StaticHosting(stack, "TestConstruct", {
691+
...defaultProps,
692+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
693+
backendHost: "backend.example.com",
694+
remapBackendPaths: [{ from: "/api/*", to: "/api/*" }],
695+
});
696+
697+
const template = Template.fromStack(stack);
698+
const distribution = template.findResources(
699+
"AWS::CloudFront::Distribution"
700+
);
701+
const distConfig =
702+
Object.values(distribution)[0].Properties.DistributionConfig;
703+
704+
const backendBehavior = distConfig.CacheBehaviors.find(
705+
(b: { PathPattern: string }) => b.PathPattern === "/api/*"
706+
);
707+
expect(backendBehavior).toBeDefined();
708+
expect(backendBehavior.ResponseHeadersPolicyId).toBeDefined();
709+
});
710+
711+
it("should apply CORS to default behavior when indexable is true", () => {
712+
const { stack } = createTestStack();
713+
new StaticHosting(stack, "TestConstruct", {
714+
...defaultProps,
715+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
716+
indexable: true,
717+
});
718+
719+
const template = Template.fromStack(stack);
720+
const distribution = template.findResources(
721+
"AWS::CloudFront::Distribution"
722+
);
723+
const distConfig =
724+
Object.values(distribution)[0].Properties.DistributionConfig;
725+
726+
// Default behavior should have response headers policy (CORS)
727+
expect(
728+
distConfig.DefaultCacheBehavior.ResponseHeadersPolicyId
729+
).toBeDefined();
730+
});
731+
});
732+
733+
describe("Indexable Configuration", () => {
734+
it("should not create NoIndexNoFollow policy when indexable is true (default)", () => {
735+
const { stack } = createTestStack();
736+
new StaticHosting(stack, "TestConstruct", defaultProps);
737+
738+
const template = Template.fromStack(stack);
739+
const policies = template.findResources(
740+
"AWS::CloudFront::ResponseHeadersPolicy"
741+
);
742+
743+
// Should not have a NoIndexNoFollow policy
744+
const hasNoIndexPolicy = Object.values(policies).some(
745+
(policy: Record<string, unknown>) => {
746+
const config = (
747+
policy.Properties as {
748+
ResponseHeadersPolicyConfig?: {
749+
CustomHeadersConfig?: {
750+
Items?: Array<{ Header: string; Value: string }>;
751+
};
752+
};
753+
}
754+
)?.ResponseHeadersPolicyConfig?.CustomHeadersConfig?.Items;
755+
return config?.some(
756+
item =>
757+
item.Header === "x-robots-tag" &&
758+
item.Value === "noindex,nofollow"
759+
);
760+
}
761+
);
762+
expect(hasNoIndexPolicy).toBe(false);
763+
});
764+
765+
it("should create NoIndexNoFollow policy when indexable is false", () => {
766+
const { stack } = createTestStack();
767+
new StaticHosting(stack, "TestConstruct", {
768+
...defaultProps,
769+
indexable: false,
770+
});
771+
772+
const template = Template.fromStack(stack);
773+
template.hasResourceProperties("AWS::CloudFront::ResponseHeadersPolicy", {
774+
ResponseHeadersPolicyConfig: {
775+
CustomHeadersConfig: {
776+
Items: [
777+
{
778+
Header: "x-robots-tag",
779+
Value: "noindex,nofollow",
780+
Override: true,
781+
},
782+
],
783+
},
784+
},
785+
});
786+
});
787+
788+
it("should combine NoIndexNoFollow with CORS when both indexable is false and corsConfig is set", () => {
789+
const { stack } = createTestStack();
790+
new StaticHosting(stack, "TestConstruct", {
791+
...defaultProps,
792+
indexable: false,
793+
corsConfig: { accessControlAllowOrigins: ["https://example.com"] },
794+
});
795+
796+
const template = Template.fromStack(stack);
797+
// Should have a policy with both noindex and CORS
798+
template.hasResourceProperties("AWS::CloudFront::ResponseHeadersPolicy", {
799+
ResponseHeadersPolicyConfig: {
800+
CustomHeadersConfig: {
801+
Items: [
802+
{
803+
Header: "x-robots-tag",
804+
Value: "noindex,nofollow",
805+
Override: true,
806+
},
807+
],
808+
},
809+
CorsConfig: {
810+
AccessControlAllowCredentials: false,
811+
AccessControlAllowHeaders: {
812+
Items: ["*"],
813+
},
814+
AccessControlAllowMethods: {
815+
Items: ["GET", "HEAD", "OPTIONS"],
816+
},
817+
AccessControlAllowOrigins: {
818+
Items: ["https://example.com"],
819+
},
820+
OriginOverride: true,
821+
},
822+
},
823+
});
824+
});
825+
826+
it("should apply NoIndexNoFollow policy to default behavior", () => {
827+
const { stack } = createTestStack();
828+
new StaticHosting(stack, "TestConstruct", {
829+
...defaultProps,
830+
indexable: false,
831+
});
832+
833+
const template = Template.fromStack(stack);
834+
const distribution = template.findResources(
835+
"AWS::CloudFront::Distribution"
836+
);
837+
const distConfig =
838+
Object.values(distribution)[0].Properties.DistributionConfig;
839+
840+
// Default behavior should have response headers policy
841+
expect(
842+
distConfig.DefaultCacheBehavior.ResponseHeadersPolicyId
843+
).toBeDefined();
844+
});
845+
});
524846

847+
// NOTE: This test creates EdgeFunctions which can cause cross-app reference issues
848+
// in subsequent tests. Keep this test section last.
849+
describe("CSP Path Behaviors", () => {
525850
it("should create CSP path behaviors", () => {
526851
const { stack } = createTestStack();
527852
new StaticHosting(stack, "TestConstruct", {

0 commit comments

Comments
 (0)