llm_topic_modelling / cdk /cdk_cloudfront_headers.py
seanpedrickcase's picture
Sync: Updated CDK code to include possibility of exporting DynamoDB logs to S3 csv output on automated basis
373207f
Raw
History Blame Contribute Delete
6.68 kB
"""CloudFront response headers policy (CSP and related security headers)."""
from __future__ import annotations
from pathlib import Path
from urllib.parse import urlparse
from aws_cdk import Duration
from aws_cdk import aws_cloudfront as cloudfront
from constructs import Construct
# Template exported from AWS; placeholders {APP-URL} and {COGNITO-APP-CLIENT-LOGIN-URL}.
_CSP_TEMPLATE = (
"default-src 'self'; script-src 'self' cdnjs.cloudflare.com 'unsafe-inline'; "
"style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; "
"img-src 'self' data:; font-src 'self' https://fonts.gstatic.com data:; "
"connect-src 'self' wss://{app_hostname} https://cdnjs.cloudflare.com; "
"form-action 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; "
"manifest-src 'self' {cognito_login_url}; upgrade-insecure-requests;"
)
RESPONSE_HEADERS_POLICY_TEMPLATE_PATH = (
Path(__file__).resolve().parent / "config" / "response-headers-policy-config.json"
)
def normalize_https_origin(url: str) -> str:
"""Return a canonical https origin (scheme + host, no path)."""
value = (url or "").strip()
if not value:
return ""
if "://" not in value:
value = f"https://{value}"
parsed = urlparse(value)
if not parsed.hostname:
return value.rstrip("/")
scheme = parsed.scheme or "https"
netloc = parsed.netloc or parsed.hostname
return f"{scheme}://{netloc}".rstrip("/")
def hostname_from_origin(origin: str) -> str:
parsed = urlparse(origin if "://" in origin else f"https://{origin}")
return parsed.hostname or origin.strip()
def cognito_hosted_ui_base_url(domain_prefix: str, region: str) -> str:
prefix = (domain_prefix or "").strip()
if not prefix:
return ""
return f"https://{prefix}.auth.{region}.amazoncognito.com"
def build_content_security_policy(
*,
app_origin: str,
cognito_login_url: str,
) -> str:
origin = normalize_https_origin(app_origin)
hostname = hostname_from_origin(origin)
cognito_url = normalize_https_origin(cognito_login_url)
return _CSP_TEMPLATE.format(app_hostname=hostname, cognito_login_url=cognito_url)
def create_secure_cloudfront_response_headers_policy(
scope: Construct,
construct_id: str,
*,
policy_name: str,
app_origin: str,
cognito_login_url: str,
comment: str = "Secure response headers with CSP for llm_topic_modeller",
) -> cloudfront.ResponseHeadersPolicy:
"""Response headers policy aligned with config/response-headers-policy-config.json."""
cors_origin = normalize_https_origin(app_origin)
csp = build_content_security_policy(
app_origin=app_origin,
cognito_login_url=cognito_login_url,
)
return cloudfront.ResponseHeadersPolicy(
scope,
construct_id,
response_headers_policy_name=policy_name,
comment=comment,
cors_behavior=cloudfront.ResponseHeadersCorsBehavior(
access_control_allow_credentials=False,
access_control_allow_headers=["*"],
access_control_allow_methods=["ALL"],
access_control_allow_origins=[cors_origin],
access_control_max_age=Duration.seconds(600),
origin_override=True,
),
security_headers_behavior=cloudfront.ResponseSecurityHeadersBehavior(
content_security_policy=cloudfront.ResponseHeadersContentSecurityPolicy(
content_security_policy=csp,
override=True,
),
content_type_options=cloudfront.ResponseHeadersContentTypeOptions(
override=True
),
frame_options=cloudfront.ResponseHeadersFrameOptions(
frame_option=cloudfront.HeadersFrameOption.SAMEORIGIN,
override=True,
),
referrer_policy=cloudfront.ResponseHeadersReferrerPolicy(
referrer_policy=cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
override=True,
),
strict_transport_security=cloudfront.ResponseHeadersStrictTransportSecurity(
access_control_max_age=Duration.seconds(31536000),
include_subdomains=True,
preload=False,
override=True,
),
xss_protection=cloudfront.ResponseHeadersXSSProtection(
protection=True,
mode_block=True,
override=True,
),
),
custom_headers_behavior=cloudfront.ResponseCustomHeadersBehavior(
custom_headers=[
cloudfront.ResponseCustomHeader(
header="Permissions-Policy",
value=(
"accelerometer=(), autoplay=(), camera=(), "
"cross-origin-isolated=(), display-capture=(), encrypted-media=(), "
"fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), "
"magnetometer=(), microphone=(), midi=(), payment=(), "
"picture-in-picture=(), publickey-credentials-get=(), "
"screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), "
"xr-spatial-tracking=()"
),
override=True,
)
]
),
remove_headers=["Server"],
)
def resolve_cloudfront_csp_urls(
*,
cognito_redirection_url: str,
cloudfront_domain: str,
cognito_user_pool_domain_prefix: str,
aws_region: str,
cognito_user_pool_login_url: str = "",
ssl_certificate_domain: str = "",
) -> tuple[str, str]:
"""
Return (app_origin, cognito_login_url) for CSP/CORS substitution.
App origin prefers COGNITO_REDIRECTION_URL (canonical browser URL), then
https://SSL_CERTIFICATE_DOMAIN, then https://CLOUDFRONT_DOMAIN.
Cognito login URL uses COGNITO_USER_POOL_LOGIN_URL when set, else the
hosted UI base URL derived from COGNITO_USER_POOL_DOMAIN_PREFIX.
"""
app_origin = normalize_https_origin(cognito_redirection_url)
if not app_origin or "placeholder" in app_origin.lower():
if ssl_certificate_domain.strip():
app_origin = normalize_https_origin(ssl_certificate_domain)
elif cloudfront_domain.strip():
app_origin = normalize_https_origin(cloudfront_domain)
login_url = (cognito_user_pool_login_url or "").strip()
if not login_url:
login_url = cognito_hosted_ui_base_url(
cognito_user_pool_domain_prefix, aws_region
)
return app_origin, normalize_https_origin(login_url)