1use std::fmt;
2
3use crate::core::{AsxError, ErrorCode, ErrorContext, InteropMode, Result, SessionContext};
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "as4")]
7use crate::crypto::wssec::WsSecCanonicalizationProfile;
8
9#[cfg(not(feature = "as4"))]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum WsSecCanonicalizationKind {
12 Exclusive,
13 Inclusive,
14}
15
16#[cfg(not(feature = "as4"))]
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct WsSecCanonicalizationProfile {
19 pub kind: WsSecCanonicalizationKind,
20 pub include_comments: bool,
21 pub inclusive_ns_prefixes: Vec<String>,
22}
23
24#[cfg(not(feature = "as4"))]
25impl Default for WsSecCanonicalizationProfile {
26 fn default() -> Self {
27 Self {
28 kind: WsSecCanonicalizationKind::Exclusive,
29 include_comments: false,
30 inclusive_ns_prefixes: Vec::new(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct CanonicalizationPolicy {
37 pub wssec: WsSecCanonicalizationProfile,
38 pub normalize_mime_headers: bool,
39}
40
41impl Default for CanonicalizationPolicy {
42 fn default() -> Self {
43 Self {
44 wssec: WsSecCanonicalizationProfile::default(),
45 normalize_mime_headers: true,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
55pub enum SecurityRequirement {
56 Signature,
58 Encryption,
60}
61
62impl SecurityRequirement {
63 pub fn as_str(self) -> &'static str {
68 match self {
69 Self::Signature => "require_signature",
70 Self::Encryption => "require_encryption",
71 }
72 }
73
74 pub const ALL: [Self; 2] = [Self::Signature, Self::Encryption];
76}
77
78impl fmt::Display for SecurityRequirement {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(self.as_str())
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub struct SecurityPolicy {
116 pub require_signature: bool,
117 pub require_encryption: bool,
118}
119
120impl SecurityPolicy {
121 pub const SIGN_AND_ENCRYPT: Self = Self {
127 require_signature: true,
128 require_encryption: true,
129 };
130
131 pub const SIGN_ONLY: Self = Self {
133 require_signature: true,
134 require_encryption: false,
135 };
136
137 pub const ENCRYPT_ONLY: Self = Self {
139 require_signature: false,
140 require_encryption: true,
141 };
142
143 pub const UNCONSTRAINED: Self = Self {
150 require_signature: false,
151 require_encryption: false,
152 };
153
154 pub fn requires(self, requirement: SecurityRequirement) -> bool {
156 match requirement {
157 SecurityRequirement::Signature => self.require_signature,
158 SecurityRequirement::Encryption => self.require_encryption,
159 }
160 }
161
162 pub fn satisfies(self, floor: Self) -> bool {
164 self.unmet_by_is_empty(floor)
165 }
166
167 fn unmet_by_is_empty(self, floor: Self) -> bool {
168 SecurityRequirement::ALL
169 .iter()
170 .all(|&req| !floor.requires(req) || self.requires(req))
171 }
172
173 pub fn unmet_by(self, candidate: Self) -> Vec<SecurityRequirement> {
178 SecurityRequirement::ALL
179 .into_iter()
180 .filter(|&req| self.requires(req) && !candidate.requires(req))
181 .collect()
182 }
183
184 pub fn relaxations_to(self, next: Self) -> Vec<SecurityRequirement> {
189 self.unmet_by(next)
190 }
191
192 pub fn strengthen(self, other: Self) -> Self {
198 Self {
199 require_signature: self.require_signature || other.require_signature,
200 require_encryption: self.require_encryption || other.require_encryption,
201 }
202 }
203
204 pub fn weaken(self, other: Self) -> Self {
206 Self {
207 require_signature: self.require_signature && other.require_signature,
208 require_encryption: self.require_encryption && other.require_encryption,
209 }
210 }
211}
212
213impl Default for SecurityPolicy {
214 fn default() -> Self {
215 Self::SIGN_AND_ENCRYPT
216 }
217}
218
219impl fmt::Display for SecurityPolicy {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 write!(
222 f,
223 "require_signature={} require_encryption={}",
224 self.require_signature, self.require_encryption
225 )
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
234pub struct ValidationPolicy {
235 pub reject_ambiguous_headers: bool,
236 pub enforce_payload_limits: bool,
237}
238
239impl Default for ValidationPolicy {
240 fn default() -> Self {
241 Self {
242 reject_ambiguous_headers: true,
243 enforce_payload_limits: true,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257pub struct As2ValidationPolicy {
258 pub require_mic: bool,
260}
261
262impl Default for As2ValidationPolicy {
263 fn default() -> Self {
264 Self { require_mic: true }
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
269pub struct ProfilePolicyOverrides {
270 pub mode: Option<InteropMode>,
271 pub canonicalization: Option<CanonicalizationPolicy>,
272 pub security: Option<SecurityPolicy>,
273 pub validation: Option<ValidationPolicy>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub as2_validation: Option<As2ValidationPolicy>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct BaseProfile {
281 pub name: String,
283 pub version: String,
289 pub mode: InteropMode,
290 pub canonicalization: CanonicalizationPolicy,
291 pub security: SecurityPolicy,
292 pub security_floor: SecurityPolicy,
308 pub validation: ValidationPolicy,
309 pub as2_validation: As2ValidationPolicy,
311}
312
313impl BaseProfile {
314 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
321 Self {
322 name: name.into(),
323 version: version.into(),
324 ..Self::default()
325 }
326 }
327
328 pub fn with_security_floor(mut self, floor: SecurityPolicy) -> Self {
330 self.security_floor = floor;
331 self
332 }
333}
334
335impl Default for BaseProfile {
336 fn default() -> Self {
339 Self {
340 name: "asx-base".to_string(),
341 version: "1.0".to_string(),
342 mode: InteropMode::Strict,
343 canonicalization: CanonicalizationPolicy::default(),
344 security: SecurityPolicy::default(),
345 security_floor: SecurityPolicy::default(),
346 validation: ValidationPolicy::default(),
347 as2_validation: As2ValidationPolicy::default(),
348 }
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct ProfileExtension {
354 pub name: String,
355 pub overrides: ProfilePolicyOverrides,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct ProfileOverride {
360 pub name: String,
361 pub overrides: ProfilePolicyOverrides,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct PartnerProfileOverlay {
366 pub name: String,
367 pub partner_id: String,
368 pub overrides: ProfilePolicyOverrides,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct ProfileStack {
373 pub base: BaseProfile,
374 pub extensions: Vec<ProfileExtension>,
375 pub overrides: Vec<ProfileOverride>,
376 pub partner_overrides: Vec<PartnerProfileOverlay>,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct RegionalProfilePack {
381 pub pack_id: String,
382 pub version: String,
383 pub applies_to_base_profile: String,
384 pub overrides: ProfilePolicyOverrides,
385}
386
387impl RegionalProfilePack {
388 pub const MAX_PACK_JSON_BYTES: usize = 512 * 1024; pub fn from_json(input: &str) -> Result<Self> {
394 if input.len() > Self::MAX_PACK_JSON_BYTES {
395 return Err(AsxError::new(
396 ErrorCode::PayloadTooLarge,
397 format!(
398 "regional profile pack JSON exceeds maximum allowed size \
399 ({} bytes, limit is {} bytes)",
400 input.len(),
401 Self::MAX_PACK_JSON_BYTES
402 ),
403 ErrorContext::new("interop_regional_pack_deserialize"),
404 ));
405 }
406 let pack: Self = serde_json::from_str(input).map_err(|err| {
407 AsxError::new(
408 ErrorCode::ParseFailed,
409 format!("failed to deserialize regional profile pack: {err}"),
410 ErrorContext::new("interop_regional_pack_deserialize"),
411 )
412 })?;
413 pack.validate()?;
414 Ok(pack)
415 }
416
417 fn validate(&self) -> Result<()> {
418 if self.pack_id.trim().is_empty() {
419 return Err(AsxError::new(
420 ErrorCode::InvalidInput,
421 "regional pack pack_id must not be empty",
422 ErrorContext::new("interop_regional_pack_validate"),
423 ));
424 }
425 if self.applies_to_base_profile.trim().is_empty() {
426 return Err(AsxError::new(
427 ErrorCode::InvalidInput,
428 format!(
429 "regional pack {} has empty applies_to_base_profile",
430 self.pack_id
431 ),
432 ErrorContext::new("interop_regional_pack_validate"),
433 ));
434 }
435 if !Self::is_semver_like(&self.version) {
436 return Err(AsxError::new(
437 ErrorCode::InvalidInput,
438 format!(
439 "regional pack {} has invalid version {}; expected semver-like x.y.z",
440 self.pack_id, self.version
441 ),
442 ErrorContext::new("interop_regional_pack_validate"),
443 ));
444 }
445 Ok(())
446 }
447
448 fn is_semver_like(version: &str) -> bool {
449 let mut parts = version.split('.');
450 let major = parts.next().unwrap_or("");
451 let minor = parts.next().unwrap_or("");
452 let patch = parts.next().unwrap_or("");
453 if parts.next().is_some() {
454 return false;
455 }
456 !major.is_empty()
457 && !minor.is_empty()
458 && !patch.is_empty()
459 && major.chars().all(|c| c.is_ascii_digit())
460 && minor.chars().all(|c| c.is_ascii_digit())
461 && patch.chars().all(|c| c.is_ascii_digit())
462 }
463
464 fn extension_name(&self) -> String {
465 format!("regional:{}@{}", self.pack_id, self.version)
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq)]
470pub struct ResolvedSessionProfile {
471 pub session: SessionContext,
472 pub effective_profile: EffectiveProfile,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
476pub struct EffectiveProfile {
477 pub name: String,
478 pub mode: InteropMode,
479 pub canonicalization: CanonicalizationPolicy,
480 pub security: SecurityPolicy,
481 pub security_floor: SecurityPolicy,
486 pub validation: ValidationPolicy,
487 pub as2_validation: As2ValidationPolicy,
489 pub snapshot: EffectivePolicySnapshot,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493pub struct EffectivePolicySnapshot {
494 pub session_id: String,
495 pub partner_id: String,
496 pub profile_name: String,
497 pub resolved_mode: InteropMode,
498 pub canonicalization: CanonicalizationPolicy,
499 pub security: SecurityPolicy,
500 pub security_floor: SecurityPolicy,
510 pub validation: ValidationPolicy,
511 #[serde(default)]
512 pub as2_validation: As2ValidationPolicy,
513 pub resolution_trace: Vec<String>,
514 pub resolution_diagnostics: Vec<ResolutionDiagnostic>,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
518pub enum ResolutionLayer {
519 Extension,
520 Override,
521 PartnerOverride,
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
525#[non_exhaustive]
526pub enum ResolutionField {
527 Mode,
528 Canonicalization,
529 Security,
530 SecurityFloor,
534 Validation,
535 As2Validation,
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540pub struct ResolutionDiagnostic {
541 pub layer: ResolutionLayer,
542 pub layer_name: String,
543 pub field: ResolutionField,
544 pub previous_value: String,
545 pub new_value: String,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
549#[non_exhaustive]
550pub enum ProfileValidationCode {
551 NoCriticalSecurityInvariant,
558 SecurityFloorViolation,
561 SecurityRelaxation,
567}
568
569impl ProfileValidationCode {
570 pub fn as_str(self) -> &'static str {
572 match self {
573 Self::NoCriticalSecurityInvariant => "no_critical_security_invariant",
574 Self::SecurityFloorViolation => "security_floor_violation",
575 Self::SecurityRelaxation => "security_relaxation",
576 }
577 }
578}
579
580impl fmt::Display for ProfileValidationCode {
581 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582 f.write_str(self.as_str())
583 }
584}
585
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
587#[non_exhaustive]
588pub enum ProfileLintCode {
589 DeadOverride,
591 SecurityRelaxation,
597}
598
599impl ProfileLintCode {
600 pub fn as_str(self) -> &'static str {
602 match self {
603 Self::DeadOverride => "dead_override",
604 Self::SecurityRelaxation => "security_relaxation",
605 }
606 }
607}
608
609impl fmt::Display for ProfileLintCode {
610 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611 f.write_str(self.as_str())
612 }
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
620pub enum ProfileLintSeverity {
621 #[default]
623 Info,
624 Warning,
626 Critical,
628}
629
630impl ProfileLintSeverity {
631 pub fn as_str(self) -> &'static str {
633 match self {
634 Self::Info => "info",
635 Self::Warning => "warning",
636 Self::Critical => "critical",
637 }
638 }
639}
640
641impl fmt::Display for ProfileLintSeverity {
642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 f.write_str(self.as_str())
644 }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct ProfileValidationIssue {
649 pub code: ProfileValidationCode,
650 pub message: String,
651 pub remediation_hint: String,
652 pub layer: String,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub partner_id: Option<String>,
661}
662
663impl fmt::Display for ProfileValidationIssue {
664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665 write!(f, "[{}] {}: {}", self.code, self.layer, self.message)?;
666 if !self.remediation_hint.is_empty() {
667 write!(f, " — hint: {}", self.remediation_hint)?;
668 }
669 Ok(())
670 }
671}
672
673impl std::error::Error for ProfileValidationIssue {}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676pub struct ProfileLintFinding {
677 pub code: ProfileLintCode,
678 pub severity: ProfileLintSeverity,
679 pub message: String,
680 pub remediation_hint: String,
681 pub layer: String,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub partner_id: Option<String>,
687}
688
689impl fmt::Display for ProfileLintFinding {
690 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691 write!(
692 f,
693 "[{}/{}] {}: {}",
694 self.severity, self.code, self.layer, self.message
695 )?;
696 if !self.remediation_hint.is_empty() {
697 write!(f, " — hint: {}", self.remediation_hint)?;
698 }
699 Ok(())
700 }
701}
702
703#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
704pub struct ProfileValidationReport {
705 pub lints: Vec<ProfileLintFinding>,
706}
707
708impl ProfileValidationReport {
709 pub fn highest_lint_severity(&self) -> Option<ProfileLintSeverity> {
711 self.lints.iter().map(|lint| lint.severity).max()
712 }
713
714 pub fn lints_at_least(
716 &self,
717 severity: ProfileLintSeverity,
718 ) -> impl Iterator<Item = &ProfileLintFinding> {
719 self.lints.iter().filter(move |l| l.severity >= severity)
720 }
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct ProfileValidationFailure {
751 pub errors: Vec<ProfileValidationIssue>,
752 pub lints: Vec<ProfileLintFinding>,
753}
754
755impl ProfileValidationFailure {
756 pub const DISPLAY_ERROR_LIMIT: usize = 1;
764
765 pub fn first_error(&self) -> Option<&ProfileValidationIssue> {
768 self.errors.first()
769 }
770
771 pub fn has_code(&self, code: ProfileValidationCode) -> bool {
773 self.errors.iter().any(|issue| issue.code == code)
774 }
775
776 pub fn affected_partners(&self) -> Vec<&str> {
779 let mut seen: Vec<&str> = Vec::new();
780 for partner in self.errors.iter().filter_map(|e| e.partner_id.as_deref()) {
781 if !seen.contains(&partner) {
782 seen.push(partner);
783 }
784 }
785 seen
786 }
787
788 pub fn report(&self) -> String {
794 use fmt::Write as _;
795
796 let mut out = format!(
797 "profile validation failed: {} error(s), {} lint(s)",
798 self.errors.len(),
799 self.lints.len()
800 );
801 for (index, issue) in self.errors.iter().enumerate() {
802 let _ = write!(out, "\n error {}. {issue}", index + 1);
804 }
805 for (index, lint) in self.lints.iter().enumerate() {
806 let _ = write!(out, "\n lint {}. {lint}", index + 1);
807 }
808 out
809 }
810}
811
812impl fmt::Display for ProfileValidationFailure {
813 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814 write!(
815 f,
816 "profile validation failed: {} error(s), {} lint(s)",
817 self.errors.len(),
818 self.lints.len()
819 )?;
820
821 let shown = self.errors.len().min(Self::DISPLAY_ERROR_LIMIT);
822 for (index, issue) in self.errors.iter().take(shown).enumerate() {
823 let separator = if index == 0 { ": " } else { "; " };
824 write!(f, "{separator}{issue}")?;
825 }
826 if self.errors.len() > shown {
827 write!(f, "; (+{} more)", self.errors.len() - shown)?;
828 }
829 Ok(())
830 }
831}
832
833impl std::error::Error for ProfileValidationFailure {
834 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
835 self.errors
836 .first()
837 .map(|issue| issue as &(dyn std::error::Error + 'static))
838 }
839}
840
841impl From<ProfileValidationFailure> for AsxError {
842 fn from(failure: ProfileValidationFailure) -> Self {
846 AsxError::new(
847 ErrorCode::PolicyViolation,
848 failure.report(),
849 ErrorContext::new("interop_profile_validate"),
850 )
851 }
852}
853
854#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)]
855pub enum DiffRiskLevel {
856 Low,
857 Medium,
858 High,
859}
860
861#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
862pub enum DiffStage {
863 Resolution,
864 Security,
865 Validation,
866 Canonicalization,
867}
868
869#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
870pub struct EffectivePolicyDiffEntry {
871 pub field: ResolutionField,
872 pub stage: DiffStage,
873 pub previous_value: String,
874 pub new_value: String,
875 pub risk: DiffRiskLevel,
876 pub rationale: String,
877}
878
879#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880pub struct ProfileImpactReport {
881 pub before_profile_name: String,
882 pub after_profile_name: String,
883 pub before_session_id: String,
884 pub after_session_id: String,
885 pub changes: Vec<EffectivePolicyDiffEntry>,
886 pub highest_risk: DiffRiskLevel,
887 pub release_blocked: bool,
888}
889
890pub type ProfileValidationResult<T> = std::result::Result<T, ProfileValidationFailure>;
891
892#[derive(Debug, Clone, PartialEq, Eq, Default)]
906#[non_exhaustive]
907pub struct ProfileValidationOptions {
908 pub security_floor: Option<SecurityPolicy>,
914 pub forbid_security_relaxation: bool,
922}
923
924impl ProfileValidationOptions {
925 pub fn with_security_floor(mut self, floor: SecurityPolicy) -> Self {
927 self.security_floor = Some(floor);
928 self
929 }
930
931 pub fn forbidding_security_relaxation(mut self) -> Self {
933 self.forbid_security_relaxation = true;
934 self
935 }
936
937 fn resolve_against(&self, base: &BaseProfile) -> ResolvedValidationOptions {
938 ResolvedValidationOptions {
939 security_floor: self.security_floor.map_or(base.security_floor, |floor| {
940 floor.strengthen(base.security_floor)
941 }),
942 forbid_security_relaxation: self.forbid_security_relaxation,
943 }
944 }
945}
946
947#[derive(Debug, Clone, PartialEq, Eq)]
981pub struct ResolvedPolicyView {
982 pub partner_id: Option<String>,
985 pub mode: InteropMode,
986 pub canonicalization: CanonicalizationPolicy,
987 pub security: SecurityPolicy,
988 pub validation: ValidationPolicy,
989 pub as2_validation: As2ValidationPolicy,
991 pub resolution_trace: Vec<String>,
992 pub resolution_diagnostics: Vec<ResolutionDiagnostic>,
993}
994
995impl ResolvedPolicyView {
996 pub fn scope_label(&self) -> &str {
998 self.partner_id.as_deref().unwrap_or("<baseline>")
999 }
1000
1001 pub fn satisfies(&self, floor: SecurityPolicy) -> bool {
1003 self.security.satisfies(floor)
1004 }
1005}
1006
1007#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1008pub enum InteropExceptionCode {
1009 As2AllowMissingMdnBoundary,
1010}
1011
1012impl InteropExceptionCode {
1013 pub fn reason_code(self) -> &'static str {
1014 match self {
1015 Self::As2AllowMissingMdnBoundary => "as2_missing_mdn_boundary",
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1021pub enum InteropGuardrailOutcome {
1022 Allowed,
1023 Denied,
1024}
1025
1026impl InteropGuardrailOutcome {
1027 pub fn as_str(self) -> &'static str {
1030 match self {
1031 Self::Allowed => "Allowed",
1032 Self::Denied => "Denied",
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Clone, PartialEq, Eq, Default)]
1038pub struct InteropExceptionPolicy {
1039 pub scoped_profile_name: Option<String>,
1040 pub allowed: Vec<InteropExceptionCode>,
1041}
1042
1043impl InteropExceptionPolicy {
1044 pub fn scoped(profile_name: impl Into<String>, allowed: Vec<InteropExceptionCode>) -> Self {
1045 Self {
1046 scoped_profile_name: Some(profile_name.into()),
1047 allowed,
1048 }
1049 }
1050
1051 pub fn allows(&self, session: &SessionContext, code: InteropExceptionCode) -> bool {
1052 match &self.scoped_profile_name {
1053 Some(scope) if scope == session.profile_name() => self.allowed.contains(&code),
1054 _ => false,
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Eq)]
1060pub enum InteropDecision {
1061 RelaxedException { reason_code: &'static str },
1062}
1063
1064pub fn evaluate_exception_guardrail(
1065 session: &SessionContext,
1066 mode: InteropMode,
1067 policy: &InteropExceptionPolicy,
1068 code: InteropExceptionCode,
1069) -> InteropGuardrailOutcome {
1070 if mode == InteropMode::Strict {
1071 return InteropGuardrailOutcome::Denied;
1072 }
1073
1074 if policy.allows(session, code) {
1075 InteropGuardrailOutcome::Allowed
1076 } else {
1077 InteropGuardrailOutcome::Denied
1078 }
1079}
1080
1081pub fn enforce_exception(
1082 session: &SessionContext,
1083 mode: InteropMode,
1084 policy: &InteropExceptionPolicy,
1085 code: InteropExceptionCode,
1086 stage: &'static str,
1087 strict_message: impl Into<String>,
1088) -> Result<InteropDecision> {
1089 let strict_message = strict_message.into();
1090 match evaluate_exception_guardrail(session, mode, policy, code) {
1091 InteropGuardrailOutcome::Allowed => Ok(InteropDecision::RelaxedException {
1092 reason_code: code.reason_code(),
1093 }),
1094 InteropGuardrailOutcome::Denied => {
1095 let message = if mode == InteropMode::Strict {
1096 strict_message
1097 } else {
1098 format!(
1099 "relaxed mode exception denied for reason {}; missing scoped exception policy",
1100 code.reason_code()
1101 )
1102 };
1103 Err(AsxError::new(
1104 ErrorCode::InteropViolation,
1105 message,
1106 ErrorContext::for_session(stage, session),
1107 ))
1108 }
1109 }
1110}
1111
1112impl EffectivePolicySnapshot {
1113 pub fn as_event_detail(&self) -> String {
1114 let trace = if self.resolution_trace.is_empty() {
1115 "none".into()
1116 } else {
1117 self.resolution_trace.join(" > ")
1118 };
1119
1120 format!(
1121 "session={} partner={} profile={} mode={:?} trace={}",
1122 self.session_id, self.partner_id, self.profile_name, self.resolved_mode, trace
1123 )
1124 }
1125
1126 pub fn to_json_pretty(&self) -> Result<String> {
1127 serde_json::to_string_pretty(self).map_err(|err| {
1128 AsxError::new(
1129 ErrorCode::ParseFailed,
1130 format!("failed to serialize effective policy snapshot: {err}"),
1131 ErrorContext::new("interop_snapshot_serialize")
1132 .with_session_and_partner(&self.session_id, &self.partner_id),
1133 )
1134 })
1135 }
1136
1137 pub fn from_json(input: &str) -> Result<Self> {
1138 serde_json::from_str(input).map_err(|err| {
1139 AsxError::new(
1140 ErrorCode::ParseFailed,
1141 format!("failed to deserialize effective policy snapshot: {err}"),
1142 ErrorContext::new("interop_snapshot_deserialize"),
1143 )
1144 })
1145 }
1146}
1147
1148impl ProfileImpactReport {
1149 pub fn to_json_pretty(&self) -> Result<String> {
1150 serde_json::to_string_pretty(self).map_err(|err| {
1151 AsxError::new(
1152 ErrorCode::ParseFailed,
1153 format!("failed to serialize profile impact report: {err}"),
1154 ErrorContext::new("interop_profile_diff_serialize"),
1155 )
1156 })
1157 }
1158}
1159
1160pub fn diff_effective_policy_snapshots(
1161 before: &EffectivePolicySnapshot,
1162 after: &EffectivePolicySnapshot,
1163) -> ProfileImpactReport {
1164 let mut changes = Vec::new();
1165
1166 if before.resolved_mode != after.resolved_mode {
1167 changes.push(EffectivePolicyDiffEntry {
1168 field: ResolutionField::Mode,
1169 stage: DiffStage::Resolution,
1170 previous_value: format!("{:?}", before.resolved_mode),
1171 new_value: format!("{:?}", after.resolved_mode),
1172 risk: DiffRiskLevel::Medium,
1173 rationale: "Interop mode changed; behavior may shift between strict and relaxed paths"
1174 .to_string(),
1175 });
1176 }
1177
1178 if before.canonicalization != after.canonicalization {
1179 changes.push(EffectivePolicyDiffEntry {
1180 field: ResolutionField::Canonicalization,
1181 stage: DiffStage::Canonicalization,
1182 previous_value: format!("{:?}", before.canonicalization),
1183 new_value: format!("{:?}", after.canonicalization),
1184 risk: DiffRiskLevel::Medium,
1185 rationale:
1186 "Canonicalization behavior changed; signature-reference interoperability may drift"
1187 .to_string(),
1188 });
1189 }
1190
1191 if before.security != after.security {
1192 let risk = if (before.security.require_signature && !after.security.require_signature)
1193 || (before.security.require_encryption && !after.security.require_encryption)
1194 {
1195 DiffRiskLevel::High
1196 } else {
1197 DiffRiskLevel::Medium
1198 };
1199 changes.push(EffectivePolicyDiffEntry {
1200 field: ResolutionField::Security,
1201 stage: DiffStage::Security,
1202 previous_value: format!("{:?}", before.security),
1203 new_value: format!("{:?}", after.security),
1204 risk,
1205 rationale:
1206 "Security invariants changed; potential weakening of signature/encryption requirements"
1207 .to_string(),
1208 });
1209 }
1210
1211 if before.security_floor != after.security_floor {
1212 let relaxed = before.security_floor.relaxations_to(after.security_floor);
1216 let (risk, rationale) = if relaxed.is_empty() {
1217 (
1218 DiffRiskLevel::Medium,
1219 "Security floor raised; overlays that previously validated may now be rejected"
1220 .to_string(),
1221 )
1222 } else {
1223 (
1224 DiffRiskLevel::High,
1225 format!(
1226 "Security floor lowered ({} no longer enforced); overlays may now relax \
1227 security without failing validation",
1228 join_requirements(&relaxed)
1229 ),
1230 )
1231 };
1232 changes.push(EffectivePolicyDiffEntry {
1233 field: ResolutionField::SecurityFloor,
1234 stage: DiffStage::Security,
1235 previous_value: format!("{:?}", before.security_floor),
1236 new_value: format!("{:?}", after.security_floor),
1237 risk,
1238 rationale,
1239 });
1240 }
1241
1242 if before.validation != after.validation {
1243 let risk = if before.validation.enforce_payload_limits
1244 && !after.validation.enforce_payload_limits
1245 {
1246 DiffRiskLevel::High
1247 } else {
1248 DiffRiskLevel::Medium
1249 };
1250 changes.push(EffectivePolicyDiffEntry {
1251 field: ResolutionField::Validation,
1252 stage: DiffStage::Validation,
1253 previous_value: format!("{:?}", before.validation),
1254 new_value: format!("{:?}", after.validation),
1255 risk,
1256 rationale: "Validation constraints changed; malformed-input acceptance may differ"
1257 .to_string(),
1258 });
1259 }
1260
1261 if before.as2_validation != after.as2_validation {
1262 changes.push(EffectivePolicyDiffEntry {
1266 field: ResolutionField::As2Validation,
1267 stage: DiffStage::Validation,
1268 previous_value: format!("{:?}", before.as2_validation),
1269 new_value: format!("{:?}", after.as2_validation),
1270 risk: DiffRiskLevel::Medium,
1271 rationale: "AS2 validation constraints changed; MDN integrity enforcement may differ"
1272 .to_string(),
1273 });
1274 }
1275
1276 let highest_risk = changes
1277 .iter()
1278 .map(|change| change.risk)
1279 .max()
1280 .unwrap_or(DiffRiskLevel::Low);
1281
1282 ProfileImpactReport {
1283 before_profile_name: before.profile_name.clone(),
1284 after_profile_name: after.profile_name.clone(),
1285 before_session_id: before.session_id.clone(),
1286 after_session_id: after.session_id.clone(),
1287 release_blocked: highest_risk == DiffRiskLevel::High,
1288 highest_risk,
1289 changes,
1290 }
1291}
1292
1293#[derive(Debug)]
1294struct ResolvedPolicyState {
1295 mode: InteropMode,
1296 canonicalization: CanonicalizationPolicy,
1297 security: SecurityPolicy,
1298 validation: ValidationPolicy,
1299 as2_validation: As2ValidationPolicy,
1300 trace: Vec<String>,
1301 diagnostics: Vec<ResolutionDiagnostic>,
1302}
1303
1304#[derive(Debug, Clone)]
1305struct EffectivePolicyState {
1306 mode: InteropMode,
1307 canonicalization: CanonicalizationPolicy,
1308 security: SecurityPolicy,
1309 validation: ValidationPolicy,
1310 as2_validation: As2ValidationPolicy,
1311}
1312
1313struct LayerFindings<'a> {
1318 errors: &'a mut Vec<ProfileValidationIssue>,
1319 lints: &'a mut Vec<ProfileLintFinding>,
1320 partner_id: Option<&'a str>,
1321}
1322
1323impl LayerFindings<'_> {
1324 fn push_error(&mut self, issue: ProfileValidationIssue) {
1325 self.errors.push(issue);
1326 }
1327
1328 fn push_lint(&mut self, lint: ProfileLintFinding) {
1329 self.lints.push(lint);
1330 }
1331
1332 fn scope_label(&self) -> String {
1333 self.partner_id.map_or_else(
1334 || "deployment baseline".to_string(),
1335 |p| format!("partner {p}"),
1336 )
1337 }
1338}
1339
1340#[derive(Debug, Clone)]
1347struct ValidationScope {
1348 state: EffectivePolicyState,
1349 last_security_layer: String,
1350 previous_security: SecurityPolicy,
1351}
1352
1353impl ValidationScope {
1354 fn apply(&mut self, qualified_layer: &str, overrides: &ProfilePolicyOverrides) {
1355 self.previous_security = self.state.security;
1356 ProfileStack::apply_effective_state_overrides(&mut self.state, overrides);
1357 if overrides.security.is_some() {
1358 self.last_security_layer = qualified_layer.to_string();
1359 }
1360 }
1361}
1362
1363#[derive(Debug, Clone, Copy)]
1365struct ResolvedValidationOptions {
1366 security_floor: SecurityPolicy,
1367 forbid_security_relaxation: bool,
1368}
1369
1370fn join_requirements(requirements: &[SecurityRequirement]) -> String {
1372 requirements
1373 .iter()
1374 .map(|req| req.as_str())
1375 .collect::<Vec<_>>()
1376 .join(" and ")
1377}
1378
1379impl ProfileStack {
1380 pub fn apply_regional_pack(&self, pack: &RegionalProfilePack) -> Result<Self> {
1381 pack.validate()?;
1382
1383 if pack.applies_to_base_profile != self.base.name {
1384 return Err(AsxError::new(
1385 ErrorCode::PolicyViolation,
1386 format!(
1387 "regional pack {}@{} targets base profile {} but active base is {}",
1388 pack.pack_id, pack.version, pack.applies_to_base_profile, self.base.name
1389 ),
1390 ErrorContext::new("interop_regional_pack_apply"),
1391 ));
1392 }
1393
1394 let mut merged = self.clone();
1395 merged.extensions.push(ProfileExtension {
1396 name: pack.extension_name(),
1397 overrides: pack.overrides.clone(),
1398 });
1399 Ok(merged)
1400 }
1401
1402 pub fn apply_regional_packs(&self, packs: &[RegionalProfilePack]) -> Result<Self> {
1403 let mut merged = self.clone();
1404 for pack in packs {
1405 merged = merged.apply_regional_pack(pack)?;
1406 }
1407 Ok(merged)
1408 }
1409
1410 fn lint_security_relaxation(
1420 findings: &mut LayerFindings<'_>,
1421 layer: &str,
1422 previous: SecurityPolicy,
1423 next: SecurityPolicy,
1424 options: &ResolvedValidationOptions,
1425 ) {
1426 let relaxed = previous.relaxations_to(next);
1427 if relaxed.is_empty() {
1428 return;
1429 }
1430
1431 let message = format!(
1432 "{layer} relaxes {} relative to the underlying layers (was {previous}, now {next})",
1433 join_requirements(&relaxed)
1434 );
1435 let hint = format!(
1436 "Remove the relaxation, or raise BaseProfile::security_floor so the weaker \
1437 policy is rejected outright rather than silently accepted for {}",
1438 findings.scope_label()
1439 );
1440
1441 if options.forbid_security_relaxation {
1442 findings.push_error(ProfileValidationIssue {
1443 code: ProfileValidationCode::SecurityRelaxation,
1444 message,
1445 remediation_hint: hint,
1446 layer: layer.to_string(),
1447 partner_id: findings.partner_id.map(str::to_string),
1448 });
1449 } else {
1450 findings.push_lint(ProfileLintFinding {
1451 code: ProfileLintCode::SecurityRelaxation,
1452 severity: ProfileLintSeverity::Critical,
1453 message,
1454 remediation_hint: hint,
1455 layer: layer.to_string(),
1456 partner_id: findings.partner_id.map(str::to_string),
1457 });
1458 }
1459 }
1460
1461 fn validate_scope_security(
1467 findings: &mut LayerFindings<'_>,
1468 layer: &str,
1469 resolved: SecurityPolicy,
1470 options: &ResolvedValidationOptions,
1471 ) {
1472 let unmet = options.security_floor.unmet_by(resolved);
1473 if !unmet.is_empty() {
1474 let scope = findings.scope_label();
1475 findings.push_error(ProfileValidationIssue {
1476 code: ProfileValidationCode::SecurityFloorViolation,
1477 message: format!(
1478 "{scope} resolves to a security policy below the profile floor: \
1479 missing {}; effective [{resolved}], floor [{}]; last set by {layer}",
1480 join_requirements(&unmet),
1481 options.security_floor
1482 ),
1483 remediation_hint: format!(
1484 "Set {} on {layer}, or lower BaseProfile::security_floor if the profile \
1485 genuinely permits the weaker policy",
1486 join_requirements(&unmet)
1487 ),
1488 layer: layer.to_string(),
1489 partner_id: findings.partner_id.map(str::to_string),
1490 });
1491 return;
1492 }
1493
1494 if resolved == SecurityPolicy::UNCONSTRAINED {
1495 let scope = findings.scope_label();
1496 findings.push_error(ProfileValidationIssue {
1497 code: ProfileValidationCode::NoCriticalSecurityInvariant,
1498 message: format!(
1499 "{scope} resolves with neither signature nor encryption required; \
1500 last set by {layer}"
1501 ),
1502 remediation_hint:
1503 "Enable at least one critical security invariant: signature or encryption"
1504 .to_string(),
1505 layer: layer.to_string(),
1506 partner_id: findings.partner_id.map(str::to_string),
1507 });
1508 }
1509 }
1510
1511 fn lint_override_layer(
1512 findings: &mut LayerFindings<'_>,
1513 layer_name: &str,
1514 current: &EffectivePolicyState,
1515 overrides: &ProfilePolicyOverrides,
1516 ) {
1517 let mut dead = |message: String, remediation_hint: &str| {
1518 findings.push_lint(ProfileLintFinding {
1519 code: ProfileLintCode::DeadOverride,
1520 severity: ProfileLintSeverity::Info,
1521 message,
1522 remediation_hint: remediation_hint.to_string(),
1523 layer: layer_name.to_string(),
1524 partner_id: findings.partner_id.map(str::to_string),
1525 });
1526 };
1527
1528 if let Some(mode) = overrides.mode
1529 && mode == current.mode
1530 {
1531 dead(
1532 format!(
1533 "{layer_name} sets mode to {mode:?}, which matches already-effective value"
1534 ),
1535 "Remove redundant override or change it to a distinct value",
1536 );
1537 }
1538
1539 if let Some(c14n) = overrides.canonicalization.as_ref()
1540 && *c14n == current.canonicalization
1541 {
1542 dead(
1543 format!("{layer_name} sets canonicalization to current effective value"),
1544 "Remove redundant canonicalization override",
1545 );
1546 }
1547
1548 if let Some(security) = overrides.security
1549 && security == current.security
1550 {
1551 dead(
1552 format!("{layer_name} sets security policy to current effective value"),
1553 "Remove redundant security override",
1554 );
1555 }
1556
1557 if let Some(validation) = overrides.validation
1558 && validation == current.validation
1559 {
1560 dead(
1561 format!("{layer_name} sets validation policy to current effective value"),
1562 "Remove redundant validation override",
1563 );
1564 }
1565
1566 if let Some(as2_validation) = overrides.as2_validation
1567 && as2_validation == current.as2_validation
1568 {
1569 dead(
1570 format!("{layer_name} sets AS2 validation policy to current effective value"),
1571 "Remove redundant as2_validation override",
1572 );
1573 }
1574 }
1575
1576 fn apply_effective_state_overrides(
1577 current: &mut EffectivePolicyState,
1578 overrides: &ProfilePolicyOverrides,
1579 ) {
1580 if let Some(mode) = overrides.mode {
1581 current.mode = mode;
1582 }
1583 if let Some(c14n) = overrides.canonicalization.as_ref() {
1584 current.canonicalization = c14n.clone();
1585 }
1586 if let Some(security) = overrides.security {
1587 current.security = security;
1588 }
1589 if let Some(validation) = overrides.validation {
1590 current.validation = validation;
1591 }
1592 if let Some(as2_validation) = overrides.as2_validation {
1593 current.as2_validation = as2_validation;
1594 }
1595 }
1596
1597 fn for_each_global_layer<F>(&self, mut f: F)
1602 where
1603 F: FnMut(ResolutionLayer, &'static str, &str, &ProfilePolicyOverrides),
1604 {
1605 for ext in &self.extensions {
1606 f(
1607 ResolutionLayer::Extension,
1608 "extension",
1609 &ext.name,
1610 &ext.overrides,
1611 );
1612 }
1613
1614 for ov in &self.overrides {
1615 f(
1616 ResolutionLayer::Override,
1617 "override",
1618 &ov.name,
1619 &ov.overrides,
1620 );
1621 }
1622 }
1623
1624 fn for_each_partner_layer<F>(&self, partner_id: Option<&str>, mut f: F)
1628 where
1629 F: FnMut(ResolutionLayer, &'static str, String, &ProfilePolicyOverrides),
1630 {
1631 let Some(partner_id) = partner_id else {
1632 return;
1633 };
1634
1635 for pov in &self.partner_overrides {
1636 if pov.partner_id != partner_id {
1637 continue;
1638 }
1639 f(
1640 ResolutionLayer::PartnerOverride,
1641 "partner_override",
1642 format!("{}:{}", pov.partner_id, pov.name),
1643 &pov.overrides,
1644 );
1645 }
1646 }
1647
1648 pub fn partner_ids(&self) -> Vec<&str> {
1651 let mut ids: Vec<&str> = Vec::new();
1652 for pov in &self.partner_overrides {
1653 if !ids.contains(&pov.partner_id.as_str()) {
1654 ids.push(&pov.partner_id);
1655 }
1656 }
1657 ids
1658 }
1659
1660 pub fn validate(&self) -> ProfileValidationResult<ProfileValidationReport> {
1675 self.validate_with(&ProfileValidationOptions::default())
1676 }
1677
1678 pub fn validate_with_floor(
1719 &self,
1720 floor: SecurityPolicy,
1721 ) -> ProfileValidationResult<ProfileValidationReport> {
1722 self.validate_with(&ProfileValidationOptions::default().with_security_floor(floor))
1723 }
1724
1725 pub fn validate_with(
1739 &self,
1740 options: &ProfileValidationOptions,
1741 ) -> ProfileValidationResult<ProfileValidationReport> {
1742 let options = options.resolve_against(&self.base);
1743
1744 let mut errors = vec![];
1745 let mut lints = vec![];
1746
1747 let baseline = {
1751 let mut findings = LayerFindings {
1752 errors: &mut errors,
1753 lints: &mut lints,
1754 partner_id: None,
1755 };
1756 let baseline = self.walk_baseline_layers(&mut findings, &options);
1757 Self::validate_scope_security(
1758 &mut findings,
1759 &baseline.last_security_layer,
1760 baseline.state.security,
1761 &options,
1762 );
1763 baseline
1764 };
1765
1766 for partner_id in self.partner_ids() {
1769 let mut scope = baseline.clone();
1770 let mut findings = LayerFindings {
1771 errors: &mut errors,
1772 lints: &mut lints,
1773 partner_id: Some(partner_id),
1774 };
1775 self.for_each_partner_layer(
1776 Some(partner_id),
1777 |_, layer_kind, layer_name, overrides| {
1778 let qualified_layer = format!("{layer_kind}:{layer_name}");
1779 Self::lint_override_layer(
1780 &mut findings,
1781 &qualified_layer,
1782 &scope.state,
1783 overrides,
1784 );
1785 scope.apply(&qualified_layer, overrides);
1786 Self::lint_security_relaxation(
1787 &mut findings,
1788 &qualified_layer,
1789 scope.previous_security,
1790 scope.state.security,
1791 &options,
1792 );
1793 },
1794 );
1795
1796 if scope.last_security_layer == baseline.last_security_layer
1801 && scope.state.security == baseline.state.security
1802 {
1803 continue;
1804 }
1805
1806 Self::validate_scope_security(
1807 &mut findings,
1808 &scope.last_security_layer,
1809 scope.state.security,
1810 &options,
1811 );
1812 }
1813
1814 if errors.is_empty() {
1815 Ok(ProfileValidationReport { lints })
1816 } else {
1817 Err(ProfileValidationFailure { errors, lints })
1818 }
1819 }
1820
1821 fn walk_baseline_layers(
1824 &self,
1825 findings: &mut LayerFindings<'_>,
1826 options: &ResolvedValidationOptions,
1827 ) -> ValidationScope {
1828 let mut scope = ValidationScope {
1829 state: EffectivePolicyState {
1830 mode: self.base.mode,
1831 canonicalization: self.base.canonicalization.clone(),
1832 security: self.base.security,
1833 validation: self.base.validation,
1834 as2_validation: self.base.as2_validation,
1835 },
1836 last_security_layer: format!("base:{}", self.base.name),
1837 previous_security: self.base.security,
1838 };
1839
1840 self.for_each_global_layer(|_, layer_kind, layer_name, overrides| {
1841 let qualified_layer = format!("{layer_kind}:{layer_name}");
1842 Self::lint_override_layer(findings, &qualified_layer, &scope.state, overrides);
1843 scope.apply(&qualified_layer, overrides);
1844 Self::lint_security_relaxation(
1845 findings,
1846 &qualified_layer,
1847 scope.previous_security,
1848 scope.state.security,
1849 options,
1850 );
1851 });
1852
1853 scope
1854 }
1855
1856 fn apply_overrides(
1857 resolved: &mut ResolvedPolicyState,
1858 layer: ResolutionLayer,
1859 layer_kind: &'static str,
1860 layer_name: &str,
1861 overrides: &ProfilePolicyOverrides,
1862 ) {
1863 if let Some(override_mode) = overrides.mode {
1864 let previous_mode = resolved.mode;
1865 resolved.mode = override_mode;
1866 resolved.trace.push(format!(
1867 "{layer_kind}:{layer_name}.mode=>{:?}",
1868 override_mode
1869 ));
1870 resolved.diagnostics.push(ResolutionDiagnostic {
1871 layer,
1872 layer_name: layer_name.to_string(),
1873 field: ResolutionField::Mode,
1874 previous_value: format!("{:?}", previous_mode),
1875 new_value: format!("{:?}", override_mode),
1876 });
1877 }
1878 if let Some(ref override_c14n) = overrides.canonicalization {
1879 let previous_c14n = resolved.canonicalization.clone();
1880 resolved.canonicalization = override_c14n.clone();
1881 resolved.trace.push(format!(
1882 "{layer_kind}:{layer_name}.canonicalization=>{:?}",
1883 override_c14n
1884 ));
1885 resolved.diagnostics.push(ResolutionDiagnostic {
1886 layer,
1887 layer_name: layer_name.to_string(),
1888 field: ResolutionField::Canonicalization,
1889 previous_value: format!("{:?}", previous_c14n),
1890 new_value: format!("{:?}", override_c14n),
1891 });
1892 }
1893 if let Some(override_security) = overrides.security {
1894 let previous_security = resolved.security;
1895 resolved.security = override_security;
1896 resolved.trace.push(format!(
1897 "{layer_kind}:{layer_name}.security=>{:?}",
1898 override_security
1899 ));
1900 resolved.diagnostics.push(ResolutionDiagnostic {
1901 layer,
1902 layer_name: layer_name.to_string(),
1903 field: ResolutionField::Security,
1904 previous_value: format!("{:?}", previous_security),
1905 new_value: format!("{:?}", override_security),
1906 });
1907 }
1908 if let Some(override_validation) = overrides.validation {
1909 let previous_validation = resolved.validation;
1910 resolved.validation = override_validation;
1911 resolved.trace.push(format!(
1912 "{layer_kind}:{layer_name}.validation=>{:?}",
1913 override_validation
1914 ));
1915 resolved.diagnostics.push(ResolutionDiagnostic {
1916 layer,
1917 layer_name: layer_name.to_string(),
1918 field: ResolutionField::Validation,
1919 previous_value: format!("{:?}", previous_validation),
1920 new_value: format!("{:?}", override_validation),
1921 });
1922 }
1923 if let Some(override_as2_validation) = overrides.as2_validation {
1924 let previous_as2_validation = resolved.as2_validation;
1925 resolved.as2_validation = override_as2_validation;
1926 resolved.trace.push(format!(
1927 "{layer_kind}:{layer_name}.as2_validation=>{:?}",
1928 override_as2_validation
1929 ));
1930 resolved.diagnostics.push(ResolutionDiagnostic {
1931 layer,
1932 layer_name: layer_name.to_string(),
1933 field: ResolutionField::As2Validation,
1934 previous_value: format!("{:?}", previous_as2_validation),
1935 new_value: format!("{:?}", override_as2_validation),
1936 });
1937 }
1938 }
1939
1940 fn resolve_state(&self, partner_id: Option<&str>) -> ResolvedPolicyState {
1947 let mut resolved = ResolvedPolicyState {
1948 mode: self.base.mode,
1949 canonicalization: self.base.canonicalization.clone(),
1950 security: self.base.security,
1951 validation: self.base.validation,
1952 as2_validation: self.base.as2_validation,
1953 trace: vec![
1954 format!("base:{}=>{:?}", self.base.name, self.base.mode),
1955 format!(
1956 "base:{}.canonicalization=>{:?}",
1957 self.base.name, self.base.canonicalization
1958 ),
1959 format!("base:{}.security=>{:?}", self.base.name, self.base.security),
1960 format!(
1961 "base:{}.validation=>{:?}",
1962 self.base.name, self.base.validation
1963 ),
1964 ],
1965 diagnostics: vec![],
1966 };
1967
1968 self.for_each_global_layer(|layer, layer_kind, layer_name, overrides| {
1969 Self::apply_overrides(&mut resolved, layer, layer_kind, layer_name, overrides);
1970 });
1971
1972 self.for_each_partner_layer(partner_id, |layer, layer_kind, layer_name, overrides| {
1973 Self::apply_overrides(&mut resolved, layer, layer_kind, &layer_name, overrides);
1974 });
1975
1976 resolved
1977 }
1978
1979 pub fn resolve_baseline(&self) -> ResolvedPolicyView {
1982 self.resolve_view(None)
1983 }
1984
1985 pub fn resolve_partner(&self, partner_id: &str) -> ResolvedPolicyView {
1991 self.resolve_view(Some(partner_id))
1992 }
1993
1994 pub fn resolve_all_partners(&self) -> Vec<ResolvedPolicyView> {
2003 let mut views = Vec::with_capacity(self.partner_overrides.len() + 1);
2004 views.push(self.resolve_baseline());
2005 for partner_id in self.partner_ids() {
2006 views.push(self.resolve_partner(partner_id));
2007 }
2008 views
2009 }
2010
2011 fn resolve_view(&self, partner_id: Option<&str>) -> ResolvedPolicyView {
2012 let resolved = self.resolve_state(partner_id);
2013 ResolvedPolicyView {
2014 partner_id: partner_id.map(str::to_string),
2015 mode: resolved.mode,
2016 canonicalization: resolved.canonicalization,
2017 security: resolved.security,
2018 validation: resolved.validation,
2019 as2_validation: resolved.as2_validation,
2020 resolution_trace: resolved.trace,
2021 resolution_diagnostics: resolved.diagnostics,
2022 }
2023 }
2024
2025 pub fn resolve(&self, session: &SessionContext) -> EffectiveProfile {
2026 let resolved = self.resolve_state(Some(session.partner_id()));
2027
2028 EffectiveProfile {
2029 name: format!("{}@{}", self.base.name, session.profile_name()),
2030 mode: resolved.mode,
2031 canonicalization: resolved.canonicalization.clone(),
2032 security: resolved.security,
2033 security_floor: self.base.security_floor,
2034 validation: resolved.validation,
2035 as2_validation: resolved.as2_validation,
2036 snapshot: EffectivePolicySnapshot {
2037 session_id: session.session_id().to_string(),
2038 partner_id: session.partner_id().to_string(),
2039 profile_name: session.profile_name().to_string(),
2040 resolved_mode: resolved.mode,
2041 canonicalization: resolved.canonicalization.clone(),
2042 security: resolved.security,
2043 security_floor: self.base.security_floor,
2044 validation: resolved.validation,
2045 as2_validation: resolved.as2_validation,
2046 resolution_trace: resolved.trace,
2047 resolution_diagnostics: resolved.diagnostics,
2048 },
2049 }
2050 }
2051
2052 pub fn resolve_for_session(&self, session: &SessionContext) -> Result<ResolvedSessionProfile> {
2053 let effective_profile = self.resolve(session);
2054 let snapshot_json = effective_profile.snapshot.to_json_pretty()?;
2055 let attached_session = session
2056 .clone()
2057 .with_effective_policy_snapshot_json(snapshot_json)?;
2058
2059 Ok(ResolvedSessionProfile {
2060 session: attached_session,
2061 effective_profile,
2062 })
2063 }
2064}
2065
2066#[cfg(test)]
2067#[cfg_attr(not(feature = "interop-relaxed"), allow(unused_imports))]
2068mod tests;