Coverage for src/lexigram/web/security/config/top_level.py: 78%
37 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Aggregate security configuration."""
3from __future__ import annotations
5from typing import ClassVar
7from lexigram.config.base import BaseConfig
8from lexigram.contracts.core.config import ConfigIssue, Environment
9from lexigram.security.constants import ENV_NESTED_DELIMITER, ENV_PREFIX
10from lexigram.validation import ConfigDict, Field, model_validator
11from lexigram.web.security.config.cors import CORSConfig
12from lexigram.web.security.config.csp import CSPConfig
13from lexigram.web.security.config.csrf import CSRFConfig
14from lexigram.web.security.config.headers import (
15 CrossOriginConfig,
16 HSTSConfig,
17 SecurityHeadersConfig,
18)
21class SecurityConfig(BaseConfig):
22 """Root HTTP security configuration for lexigram-web.
24 Aggregates CORS, CSRF, security-headers, and HTTP security-policy
25 sub-configs into a single object.
27 Attributes:
28 cors: CORS policy configuration.
29 csrf: CSRF protection configuration.
30 headers: Low-level security response headers.
31 hsts: Structured HSTS configuration.
32 csp: Content Security Policy configuration.
33 cross_origin: Cross-origin isolation policy headers.
34 referrer_policy: Referrer-Policy header value.
35 custom_headers: Additional response headers emitted verbatim.
36 permissions_policy: Permissions-Policy directive map.
37 enable_csrf: Convenience flag — enable/disable CSRF.
38 enable_cors: Convenience flag — enable/disable CORS.
39 """
41 model_config: ClassVar[ConfigDict] = ConfigDict(
42 env_prefix=ENV_PREFIX, # type: ignore[typeddict-unknown-key]
43 env_nested_delimiter=ENV_NESTED_DELIMITER,
44 extra="ignore",
45 )
47 enabled: bool = Field(default=True, description="Enable the security subsystem")
49 cors: CORSConfig = Field(default_factory=CORSConfig)
50 csrf: CSRFConfig = Field(default_factory=CSRFConfig)
51 headers: SecurityHeadersConfig = Field(default_factory=SecurityHeadersConfig)
53 # Convenience flags
54 enable_csrf: bool = Field(default=True)
55 enable_cors: bool = Field(default=True)
57 # Host validation (fail-closed; production requires a non-empty list)
58 allowed_hosts: list[str] = Field(
59 default_factory=list,
60 description="Hostnames permitted to reach the application. Empty by "
61 "default; must be configured before production deployment.",
62 )
64 # HTTP security-policy sub-configs
65 hsts: HSTSConfig = Field(
66 default_factory=HSTSConfig,
67 description="HSTS configuration (enabled, max_age, subdomains, preload)",
68 )
69 csp: CSPConfig = Field(
70 default_factory=CSPConfig,
71 description="Content Security Policy configuration",
72 )
73 cross_origin: CrossOriginConfig = Field(
74 default_factory=CrossOriginConfig,
75 description="Cross-origin isolation policy headers",
76 )
77 referrer_policy: str = Field(
78 default="strict-origin-when-cross-origin",
79 description="Referrer-Policy header value",
80 )
81 custom_headers: dict[str, str] = Field(
82 default_factory=lambda: {
83 "x-content-type-options": "nosniff",
84 "x-frame-options": "DENY",
85 "x-xss-protection": "1; mode=block",
86 },
87 description="Additional HTTP response headers emitted verbatim",
88 )
89 permissions_policy: dict[str, str] = Field(
90 default_factory=lambda: {
91 "geolocation": "()",
92 "microphone": "()",
93 "camera": "()",
94 "payment": "()",
95 "usb": "()",
96 },
97 description="Permissions-Policy directive map",
98 )
100 def validate_for_environment(
101 self, env: Environment | None = None
102 ) -> list[ConfigIssue]:
103 """Aggregate validation from all sub-configs."""
104 issues: list[ConfigIssue] = []
105 issues.extend(self.cors.validate_for_environment(env))
106 issues.extend(self.csrf.validate_for_environment(env))
107 issues.extend(self.headers.validate_for_environment(env))
108 return issues
110 @model_validator(mode="after")
111 def _sync_csrf_enable_flag(self) -> SecurityConfig:
112 """Make ``enable_csrf`` authoritative for disabling CSRF.
114 An explicit ``enable_csrf=False`` overrides ``csrf.enabled`` so the
115 convenience flag can never disagree with the wired sub-config; an
116 explicit ``csrf`` (or ``csrf.enabled``) still wins when ``enable_csrf``
117 is left at its default ``True``.
118 """
119 if not self.enable_csrf:
120 self.csrf.enabled = False
121 return self
124__all__ = [
125 "SecurityConfig",
126]