| """CloudFront in RedactionStack: magic-link auth and distribution synth.""" |
|
|
| import sys |
| from pathlib import Path |
|
|
| CDK_DIR = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(CDK_DIR)) |
|
|
| from cdk_cloudfront_auth import ( |
| build_forwarded_host_viewer_request_js, |
| build_magic_link_viewer_request_js, |
| ) |
| from cdk_cloudfront_distribution import ( |
| express_endpoint_hostname, |
| parse_geo_restriction_locations, |
| ) |
|
|
|
|
| def test_express_endpoint_hostname(): |
| assert ( |
| express_endpoint_hostname("https://abc123.eu-west-2.on.aws") |
| == "abc123.eu-west-2.on.aws" |
| ) |
| assert express_endpoint_hostname("abc123.eu-west-2.on.aws") == ( |
| "abc123.eu-west-2.on.aws" |
| ) |
|
|
|
|
| def test_parse_geo_restriction_locations(): |
| assert parse_geo_restriction_locations("GB") == ["GB"] |
| assert parse_geo_restriction_locations("gb, us") == ["GB", "US"] |
| assert parse_geo_restriction_locations("") is None |
|
|
|
|
| def test_magic_link_viewer_request_js_embeds_token_and_cookie(): |
| js = build_magic_link_viewer_request_js( |
| token="a1b2c3d4e5f6789012345678901234ab", |
| cookie_name="doc-redaction-auth", |
| cookie_max_age_sec=604800, |
| ) |
| assert "a1b2c3d4e5f6789012345678901234ab" in js |
| assert "doc-redaction-auth" in js |
| assert "604800" in js |
| assert "statusCode: 302" in js |
| assert "statusCode: 401" in js |
|
|
|
|
| def test_magic_link_viewer_request_js_forwards_viewer_host(): |
| """Authorized requests must forward the viewer host so Gradio builds asset URLs |
| against the CloudFront domain, not the *.on.aws origin host. The proto is NOT set |
| here: x-forwarded-proto is a CloudFront-disallowed edge-function header (HTTP 502). |
| """ |
| js = build_magic_link_viewer_request_js( |
| token="a1b2c3d4e5f6789012345678901234ab", |
| cookie_name="doc-redaction-auth", |
| cookie_max_age_sec=604800, |
| ) |
| assert "x-forwarded-host" in js |
| assert "x-forwarded-proto" not in js |
|
|
|
|
| def test_forwarded_host_viewer_request_js_sets_headers(): |
| js = build_forwarded_host_viewer_request_js() |
| assert "x-forwarded-host" in js |
| |
| |
| assert "x-forwarded-proto" not in js |
| assert "request.headers.host.value" in js |
| assert "return request;" in js |
|
|
|
|
| def test_cloudfront_without_magic_link_still_forwards_host(): |
| """auth_mode='none' must still attach a viewer-request function that forwards |
| the viewer host (otherwise Gradio assets 404/time out behind CloudFront).""" |
| template = _synth_cloudfront_with_headers(attach=False) |
| template.resource_count_is("AWS::CloudFront::Function", 1) |
|
|
|
|
| def test_express_cloudfront_synth_no_waf(): |
| from aws_cdk import App, Environment, Stack, assertions |
| from cdk_cloudfront_distribution import create_redaction_cloudfront_distribution |
| from cdk_functions import managed_resource_removal_policy |
|
|
| app = App() |
| stack = Stack( |
| app, |
| "ExpressCloudFrontTest", |
| env=Environment(account="123456789012", region="eu-west-2"), |
| ) |
|
|
| create_redaction_cloudfront_distribution( |
| stack, |
| "Cf", |
| distribution_comment="test-dist", |
| cognito_redirection_url="https://main.example.on.aws", |
| cloudfront_domain="d111.cloudfront.net", |
| cognito_user_pool_domain_prefix="demo", |
| aws_region="eu-west-2", |
| cognito_user_pool_login_url="", |
| ssl_certificate_domain="", |
| enable_secure_response_headers=False, |
| geo_restriction_raw="GB", |
| enable_cloudfront_waf=False, |
| web_acl_name="test-waf", |
| auth_mode="magic-link", |
| magic_link_cookie_name="doc-redaction-auth", |
| magic_link_cookie_max_age_sec=604800, |
| custom_header_name="", |
| custom_header_value="", |
| cdk_prefix="Test", |
| resource_removal_policy=managed_resource_removal_policy(), |
| main_express_endpoint="https://main.example.on.aws", |
| agentic_express_endpoint="https://agentic.example.on.aws", |
| agentic_path_prefix="/agent", |
| ) |
|
|
| template = assertions.Template.from_stack(stack) |
| template.resource_count_is("AWS::WAFv2::WebACL", 0) |
| template.resource_count_is("AWS::CloudFront::Distribution", 1) |
| template.resource_count_is("AWS::CloudFront::Function", 1) |
| resources = template.to_json()["Resources"] |
| dist = next( |
| r for r in resources.values() if r["Type"] == "AWS::CloudFront::Distribution" |
| ) |
| dist_config = dist["Properties"]["DistributionConfig"] |
| behaviors = dist_config["CacheBehaviors"] |
| assert len(behaviors) >= 2 |
|
|
| |
| |
| |
| all_viewer_except_host = "b689b0a8-53d0-40ab-baf2-68738e2966ac" |
| assert ( |
| dist_config["DefaultCacheBehavior"]["OriginRequestPolicyId"] |
| == all_viewer_except_host |
| ) |
| for behavior in behaviors: |
| assert behavior["OriginRequestPolicyId"] == all_viewer_except_host |
|
|
|
|
| def test_magic_link_emits_agent_app_login_url_output(): |
| """When the agent (Pi) app has a dedicated CloudFront behavior, the stack must |
| expose AgentRedactionUrl / AgentRedactionLoginUrl so operators can unlock and |
| open the agent UI directly (magic-link cookie is domain-wide).""" |
| from aws_cdk import App, Environment, Stack, assertions |
| from cdk_cloudfront_distribution import create_redaction_cloudfront_distribution |
| from cdk_functions import managed_resource_removal_policy |
|
|
| app = App() |
| stack = Stack( |
| app, |
| "AgentLoginUrlTest", |
| env=Environment(account="123456789012", region="eu-west-2"), |
| ) |
| create_redaction_cloudfront_distribution( |
| stack, |
| "Cf", |
| distribution_comment="test-dist", |
| cognito_redirection_url="https://main.example.on.aws", |
| cloudfront_domain="d111.cloudfront.net", |
| cognito_user_pool_domain_prefix="demo", |
| aws_region="eu-west-2", |
| cognito_user_pool_login_url="", |
| ssl_certificate_domain="", |
| enable_secure_response_headers=False, |
| geo_restriction_raw="GB", |
| enable_cloudfront_waf=False, |
| web_acl_name="test-waf", |
| auth_mode="magic-link", |
| magic_link_cookie_name="doc-redaction-auth", |
| magic_link_cookie_max_age_sec=604800, |
| custom_header_name="", |
| custom_header_value="", |
| cdk_prefix="Test", |
| resource_removal_policy=managed_resource_removal_policy(), |
| main_express_endpoint="https://main.example.on.aws", |
| agentic_express_endpoint="https://agentic.example.on.aws", |
| agentic_path_prefix="/agent", |
| ) |
| outputs = assertions.Template.from_stack(stack).to_json().get("Outputs", {}) |
| assert "AgentRedactionUrl" in outputs |
| assert "AgentRedactionLoginUrl" in outputs |
|
|
|
|
| def test_no_agent_output_when_agent_shares_main_origin(): |
| """No separate agent behavior (agentic host == main host) -> no agent URL output.""" |
| from aws_cdk import App, Environment, Stack, assertions |
| from cdk_cloudfront_distribution import create_redaction_cloudfront_distribution |
| from cdk_functions import managed_resource_removal_policy |
|
|
| app = App() |
| stack = Stack( |
| app, |
| "NoAgentOutputTest", |
| env=Environment(account="123456789012", region="eu-west-2"), |
| ) |
| create_redaction_cloudfront_distribution( |
| stack, |
| "Cf", |
| distribution_comment="test-dist", |
| cognito_redirection_url="https://main.example.on.aws", |
| cloudfront_domain="d111.cloudfront.net", |
| cognito_user_pool_domain_prefix="demo", |
| aws_region="eu-west-2", |
| cognito_user_pool_login_url="", |
| ssl_certificate_domain="", |
| enable_secure_response_headers=False, |
| geo_restriction_raw="GB", |
| enable_cloudfront_waf=False, |
| web_acl_name="test-waf", |
| auth_mode="magic-link", |
| magic_link_cookie_name="doc-redaction-auth", |
| magic_link_cookie_max_age_sec=604800, |
| custom_header_name="", |
| custom_header_value="", |
| cdk_prefix="Test", |
| resource_removal_policy=managed_resource_removal_policy(), |
| main_express_endpoint="https://main.example.on.aws", |
| agentic_express_endpoint="", |
| agentic_path_prefix="/agent", |
| ) |
| outputs = assertions.Template.from_stack(stack).to_json().get("Outputs", {}) |
| assert "RedactionLoginUrl" in outputs |
| assert "AgentRedactionUrl" not in outputs |
| assert "AgentRedactionLoginUrl" not in outputs |
|
|
|
|
| def test_express_origins_set_static_forwarded_proto_header(): |
| """Every Express origin must carry a static X-Forwarded-Proto: https custom header |
| so Gradio emits https asset URLs (the edge function can't set this header).""" |
| from aws_cdk import App, Environment, Stack, assertions |
| from cdk_cloudfront_distribution import create_redaction_cloudfront_distribution |
| from cdk_functions import managed_resource_removal_policy |
|
|
| app = App() |
| stack = Stack( |
| app, |
| "ExpressFwdProtoTest", |
| env=Environment(account="123456789012", region="eu-west-2"), |
| ) |
| create_redaction_cloudfront_distribution( |
| stack, |
| "Cf", |
| distribution_comment="test-dist", |
| cognito_redirection_url="https://main.example.on.aws", |
| cloudfront_domain="d111.cloudfront.net", |
| cognito_user_pool_domain_prefix="demo", |
| aws_region="eu-west-2", |
| cognito_user_pool_login_url="", |
| ssl_certificate_domain="", |
| enable_secure_response_headers=False, |
| geo_restriction_raw="GB", |
| enable_cloudfront_waf=False, |
| web_acl_name="test-waf", |
| auth_mode="magic-link", |
| magic_link_cookie_name="doc-redaction-auth", |
| magic_link_cookie_max_age_sec=604800, |
| custom_header_name="", |
| custom_header_value="", |
| cdk_prefix="Test", |
| resource_removal_policy=managed_resource_removal_policy(), |
| main_express_endpoint="https://main.example.on.aws", |
| agentic_express_endpoint="https://agentic.example.on.aws", |
| agentic_path_prefix="/agent", |
| ) |
| template = assertions.Template.from_stack(stack) |
| dist = next( |
| r |
| for r in template.to_json()["Resources"].values() |
| if r["Type"] == "AWS::CloudFront::Distribution" |
| ) |
| origins_cfg = dist["Properties"]["DistributionConfig"]["Origins"] |
| assert origins_cfg, "expected at least one origin" |
| for origin in origins_cfg: |
| header_pairs = { |
| h["HeaderName"]: h["HeaderValue"] |
| for h in origin.get("OriginCustomHeaders", []) |
| } |
| assert header_pairs.get("X-Forwarded-Proto") == "https" |
|
|
|
|
| def _synth_cloudfront_with_headers(attach: bool): |
| from aws_cdk import App, Environment, Stack, assertions |
| from cdk_cloudfront_distribution import create_redaction_cloudfront_distribution |
| from cdk_functions import managed_resource_removal_policy |
|
|
| app = App() |
| stack = Stack( |
| app, |
| f"HeadersTest{'Attach' if attach else 'Detach'}", |
| env=Environment(account="123456789012", region="eu-west-2"), |
| ) |
| create_redaction_cloudfront_distribution( |
| stack, |
| "Cf", |
| distribution_comment="test-dist", |
| cognito_redirection_url="https://main.example.on.aws", |
| cloudfront_domain="d111.cloudfront.net", |
| cognito_user_pool_domain_prefix="demo", |
| aws_region="eu-west-2", |
| cognito_user_pool_login_url="", |
| ssl_certificate_domain="", |
| enable_secure_response_headers=True, |
| attach_secure_response_headers=attach, |
| geo_restriction_raw="GB", |
| enable_cloudfront_waf=False, |
| web_acl_name="test-waf", |
| auth_mode="none", |
| magic_link_cookie_name="doc-redaction-auth", |
| magic_link_cookie_max_age_sec=604800, |
| custom_header_name="", |
| custom_header_value="", |
| cdk_prefix="Test", |
| resource_removal_policy=managed_resource_removal_policy(), |
| main_express_endpoint="https://main.example.on.aws", |
| agentic_express_endpoint="https://agentic.example.on.aws", |
| agentic_path_prefix="/agent", |
| ) |
| return assertions.Template.from_stack(stack) |
|
|
|
|
| def _behavior_response_headers_ids(template) -> list: |
| dist = next( |
| r |
| for r in template.to_json()["Resources"].values() |
| if r["Type"] == "AWS::CloudFront::Distribution" |
| ) |
| config = dist["Properties"]["DistributionConfig"] |
| behaviors = [config["DefaultCacheBehavior"], *config.get("CacheBehaviors", [])] |
| return [b.get("ResponseHeadersPolicyId") for b in behaviors] |
|
|
|
|
| def test_response_headers_policy_created_but_detached_by_default(): |
| template = _synth_cloudfront_with_headers(attach=False) |
| |
| template.resource_count_is("AWS::CloudFront::ResponseHeadersPolicy", 1) |
| |
| ids = _behavior_response_headers_ids(template) |
| assert all(policy_id is None for policy_id in ids), ids |
|
|
|
|
| def test_response_headers_policy_attached_when_requested(): |
| template = _synth_cloudfront_with_headers(attach=True) |
| template.resource_count_is("AWS::CloudFront::ResponseHeadersPolicy", 1) |
| ids = _behavior_response_headers_ids(template) |
| assert ids, ids |
| assert all(policy_id is not None for policy_id in ids), ids |
|
|