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

1"""Aggregate security configuration.""" 

2 

3from __future__ import annotations 

4 

5from typing import ClassVar 

6 

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) 

19 

20 

21class SecurityConfig(BaseConfig): 

22 """Root HTTP security configuration for lexigram-web. 

23 

24 Aggregates CORS, CSRF, security-headers, and HTTP security-policy 

25 sub-configs into a single object. 

26 

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 """ 

40 

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 ) 

46 

47 enabled: bool = Field(default=True, description="Enable the security subsystem") 

48 

49 cors: CORSConfig = Field(default_factory=CORSConfig) 

50 csrf: CSRFConfig = Field(default_factory=CSRFConfig) 

51 headers: SecurityHeadersConfig = Field(default_factory=SecurityHeadersConfig) 

52 

53 # Convenience flags 

54 enable_csrf: bool = Field(default=True) 

55 enable_cors: bool = Field(default=True) 

56 

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 ) 

63 

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 ) 

99 

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 

109 

110 @model_validator(mode="after") 

111 def _sync_csrf_enable_flag(self) -> SecurityConfig: 

112 """Make ``enable_csrf`` authoritative for disabling CSRF. 

113 

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 

122 

123 

124__all__ = [ 

125 "SecurityConfig", 

126]