Skip to main content

rullst_core/
production.rs

1//! Canonical production middleware ordering contract.
2//!
3//! The framework owns the transport and baseline-security stages. Session,
4//! authentication, tenant resolution and authorization remain application
5//! layers because their state and policies are domain-specific.
6
7/// A stage in the canonical inbound production middleware chain.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum ProductionMiddlewareStage {
11    /// Accept forwarded identity only through an explicitly trusted proxy policy.
12    TrustedProxy,
13    /// Reject request bodies above the route/application limit before buffering.
14    BodyLimit,
15    /// Accept or generate a validated request correlation identifier.
16    RequestId,
17    /// Start request tracing after correlation identity exists.
18    Tracing,
19    /// Wrap every inner response with the strict secure-header baseline.
20    SecureHeaders,
21    /// Apply an explicit origin allowlist; an empty list authorizes no cross-origin caller.
22    Cors,
23    /// Inspect supported, bounded request data with the configured WAF/RASP policy.
24    WafRasp,
25    /// Validate browser-originated state-changing requests.
26    Csrf,
27    /// Resolve and cryptographically validate the application session.
28    Session,
29    /// Construct the authenticated subject from validated credentials.
30    Authentication,
31    /// Resolve tenant membership from the authenticated subject.
32    Tenant,
33    /// Enforce role, permission and object ownership.
34    Authorization,
35    /// Rate limit by authenticated identity when present, otherwise by direct peer.
36    RateLimit,
37    /// Dispatch the application handler only after every applicable guard.
38    Handler,
39}
40
41/// Typed, stable description of the v12 production middleware preset.
42///
43/// This type documents the outer-to-inner request order. It deliberately does
44/// not fabricate generic authentication or authorization: applications mount
45/// those domain layers in the slots declared here.
46#[derive(Debug, Clone, Copy, Default)]
47pub struct ProductionPreset;
48
49impl ProductionPreset {
50    /// Canonical outer-to-inner request order for production applications.
51    pub const MIDDLEWARE_ORDER: &'static [ProductionMiddlewareStage] = &[
52        ProductionMiddlewareStage::TrustedProxy,
53        ProductionMiddlewareStage::BodyLimit,
54        ProductionMiddlewareStage::RequestId,
55        ProductionMiddlewareStage::Tracing,
56        ProductionMiddlewareStage::SecureHeaders,
57        ProductionMiddlewareStage::Cors,
58        ProductionMiddlewareStage::WafRasp,
59        ProductionMiddlewareStage::Csrf,
60        ProductionMiddlewareStage::Session,
61        ProductionMiddlewareStage::Authentication,
62        ProductionMiddlewareStage::Tenant,
63        ProductionMiddlewareStage::Authorization,
64        ProductionMiddlewareStage::RateLimit,
65        ProductionMiddlewareStage::Handler,
66    ];
67
68    /// Returns the canonical outer-to-inner middleware order.
69    pub const fn middleware_order() -> &'static [ProductionMiddlewareStage] {
70        Self::MIDDLEWARE_ORDER
71    }
72
73    /// Creates the fail-closed Academy boundary assessment.
74    ///
75    /// Every Academy-specific requirement starts as [`AcademyCheckStatus::NotEvaluated`].
76    /// Applications may attach observations from their own topology and call
77    /// [`AcademyProductionPreset::validate`]; validation succeeds only when every requirement
78    /// has exactly one explicit [`AcademyCheckStatus::Pass`]. This is an integration contract,
79    /// not an automatic security certification.
80    pub const fn academy() -> AcademyProductionPreset {
81        AcademyProductionPreset
82    }
83}
84
85/// An Academy domain boundary that must be evaluated by the deployed application.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87#[non_exhaustive]
88pub enum AcademyProductionRequirement {
89    /// Identity originates from a validated server-side session or reviewed gateway.
90    AuthenticatedIdentity,
91    /// The subject has current membership in the selected school/tenant.
92    SchoolMembership,
93    /// Protected content is gated by a current server-side entitlement.
94    ActiveEntitlement,
95    /// Object ownership/role checks run before data or side effects are exposed.
96    ObjectAuthorization,
97    /// Database, cache, queue, storage, search and telemetry are tenant-isolated.
98    TenantIsolation,
99    /// Assessments and grades are validated or calculated authoritatively on the server.
100    ServerValidatedAssessment,
101    /// Score events are authenticated, versioned, bounded and idempotent.
102    IdempotentScoreEvents,
103    /// Domain events use a durable outbox and idempotent automation handlers.
104    DurableAutomation,
105    /// Administrative and grade/score mutations have a durable actor-bound audit trail.
106    DurableAdminAudit,
107    /// Uploads and active content pass type-specific quarantine/scanning policy.
108    SafeContentPipeline,
109    /// Consent, minimization, retention, export and deletion have been evaluated.
110    PrivacyLifecycle,
111    /// Abuse controls cover the deployment's distributed identity/origin boundary.
112    DistributedAbuseControl,
113}
114
115impl AcademyProductionRequirement {
116    /// Complete requirement inventory used by the fail-closed assessment.
117    pub const ALL: &'static [Self] = &[
118        Self::AuthenticatedIdentity,
119        Self::SchoolMembership,
120        Self::ActiveEntitlement,
121        Self::ObjectAuthorization,
122        Self::TenantIsolation,
123        Self::ServerValidatedAssessment,
124        Self::IdempotentScoreEvents,
125        Self::DurableAutomation,
126        Self::DurableAdminAudit,
127        Self::SafeContentPipeline,
128        Self::PrivacyLifecycle,
129        Self::DistributedAbuseControl,
130    ];
131
132    /// Stable snake-case name used by diagnostics and evidence files.
133    pub const fn as_str(self) -> &'static str {
134        match self {
135            Self::AuthenticatedIdentity => "authenticated_identity",
136            Self::SchoolMembership => "school_membership",
137            Self::ActiveEntitlement => "active_entitlement",
138            Self::ObjectAuthorization => "object_authorization",
139            Self::TenantIsolation => "tenant_isolation",
140            Self::ServerValidatedAssessment => "server_validated_assessment",
141            Self::IdempotentScoreEvents => "idempotent_score_events",
142            Self::DurableAutomation => "durable_automation",
143            Self::DurableAdminAudit => "durable_admin_audit",
144            Self::SafeContentPipeline => "safe_content_pipeline",
145            Self::PrivacyLifecycle => "privacy_lifecycle",
146            Self::DistributedAbuseControl => "distributed_abuse_control",
147        }
148    }
149}
150
151/// Evidence status for one Academy production boundary.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[non_exhaustive]
154pub enum AcademyCheckStatus {
155    /// A concrete check ran and passed within its declared scope.
156    Pass,
157    /// A concrete check ran and failed.
158    Fail,
159    /// The check was deliberately skipped with an application-owned justification.
160    Skipped,
161    /// No reliable check or evidence was supplied.
162    NotEvaluated,
163}
164
165impl AcademyCheckStatus {
166    /// Stable uppercase diagnostic label.
167    pub const fn as_str(self) -> &'static str {
168        match self {
169            Self::Pass => "PASS",
170            Self::Fail => "FAIL",
171            Self::Skipped => "SKIPPED",
172            Self::NotEvaluated => "NOT_EVALUATED",
173        }
174    }
175}
176
177/// One normalized Academy boundary assessment.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179#[non_exhaustive]
180pub struct AcademyBoundaryAssessment {
181    requirement: AcademyProductionRequirement,
182    status: AcademyCheckStatus,
183}
184
185impl AcademyBoundaryAssessment {
186    /// Assessed requirement.
187    pub const fn requirement(&self) -> AcademyProductionRequirement {
188        self.requirement
189    }
190
191    /// Normalized status, defaulting to `NOT_EVALUATED` when no observation was supplied.
192    pub const fn status(&self) -> AcademyCheckStatus {
193        self.status
194    }
195}
196
197/// Fail-closed Academy preset validation error.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
199#[non_exhaustive]
200pub enum AcademyPresetError {
201    /// The caller supplied conflicting or duplicate evidence for one requirement.
202    #[error("Academy requirement {0:?} was supplied more than once")]
203    DuplicateRequirement(AcademyProductionRequirement),
204    /// A requirement was absent, skipped or failed rather than explicitly passing.
205    #[error("Academy requirement {requirement:?} is {status:?}, not PASS")]
206    RequirementNotPassed {
207        /// Requirement that prevented validation.
208        requirement: AcademyProductionRequirement,
209        /// Observed or defaulted status.
210        status: AcademyCheckStatus,
211    },
212}
213
214/// Fail-closed Academy domain assessment layered on the canonical production order.
215#[derive(Debug, Clone, Copy, Default)]
216pub struct AcademyProductionPreset;
217
218impl AcademyProductionPreset {
219    /// Uses the same outer-to-inner HTTP order as the base production preset.
220    pub const fn middleware_order(&self) -> &'static [ProductionMiddlewareStage] {
221        ProductionPreset::middleware_order()
222    }
223
224    /// Normalizes application observations against the complete requirement inventory.
225    ///
226    /// Missing observations are emitted as `NOT_EVALUATED`; duplicate observations are rejected.
227    pub fn assess(
228        &self,
229        observations: impl IntoIterator<Item = (AcademyProductionRequirement, AcademyCheckStatus)>,
230    ) -> Result<Vec<AcademyBoundaryAssessment>, AcademyPresetError> {
231        let observations = observations.into_iter().collect::<Vec<_>>();
232        for (index, (requirement, _)) in observations.iter().enumerate() {
233            if observations[index + 1..]
234                .iter()
235                .any(|(candidate, _)| candidate == requirement)
236            {
237                return Err(AcademyPresetError::DuplicateRequirement(*requirement));
238            }
239        }
240
241        Ok(AcademyProductionRequirement::ALL
242            .iter()
243            .map(|requirement| AcademyBoundaryAssessment {
244                requirement: *requirement,
245                status: observations
246                    .iter()
247                    .find_map(|(candidate, status)| (candidate == requirement).then_some(*status))
248                    .unwrap_or(AcademyCheckStatus::NotEvaluated),
249            })
250            .collect())
251    }
252
253    /// Validates only an explicit all-`PASS` assessment.
254    pub fn validate(
255        &self,
256        observations: impl IntoIterator<Item = (AcademyProductionRequirement, AcademyCheckStatus)>,
257    ) -> Result<(), AcademyPresetError> {
258        for assessment in self.assess(observations)? {
259            if assessment.status != AcademyCheckStatus::Pass {
260                return Err(AcademyPresetError::RequirementNotPassed {
261                    requirement: assessment.requirement,
262                    status: assessment.status,
263                });
264            }
265        }
266        Ok(())
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::{
273        AcademyCheckStatus, AcademyPresetError, AcademyProductionRequirement,
274        ProductionMiddlewareStage, ProductionPreset,
275    };
276    use std::collections::HashSet;
277
278    #[test]
279    fn production_order_is_complete_unique_and_ends_at_the_handler() {
280        let order = ProductionPreset::middleware_order();
281        let unique = order.iter().copied().collect::<HashSet<_>>();
282
283        assert_eq!(order.len(), 14);
284        assert_eq!(unique.len(), order.len());
285        assert_eq!(
286            order.first(),
287            Some(&ProductionMiddlewareStage::TrustedProxy)
288        );
289        assert_eq!(order.last(), Some(&ProductionMiddlewareStage::Handler));
290    }
291
292    #[test]
293    fn identity_is_validated_before_tenant_authorization_and_rate_limit() {
294        let order = ProductionPreset::middleware_order();
295        let expected_identity_order = [
296            ProductionMiddlewareStage::Session,
297            ProductionMiddlewareStage::Authentication,
298            ProductionMiddlewareStage::Tenant,
299            ProductionMiddlewareStage::Authorization,
300            ProductionMiddlewareStage::RateLimit,
301        ];
302        let observed_identity_order = order
303            .iter()
304            .copied()
305            .filter(|stage| expected_identity_order.contains(stage))
306            .collect::<Vec<_>>();
307
308        assert_eq!(observed_identity_order, expected_identity_order);
309    }
310
311    #[test]
312    fn academy_preset_defaults_to_not_evaluated_and_fails_closed() -> Result<(), AcademyPresetError>
313    {
314        let preset = ProductionPreset::academy();
315        let assessments = preset.assess([])?;
316
317        assert_eq!(
318            preset.middleware_order(),
319            ProductionPreset::middleware_order()
320        );
321        assert_eq!(assessments.len(), AcademyProductionRequirement::ALL.len());
322        assert!(
323            assessments
324                .iter()
325                .all(|assessment| assessment.status() == AcademyCheckStatus::NotEvaluated)
326        );
327        assert!(matches!(
328            preset.validate([]),
329            Err(AcademyPresetError::RequirementNotPassed {
330                status: AcademyCheckStatus::NotEvaluated,
331                ..
332            })
333        ));
334        Ok(())
335    }
336
337    #[test]
338    fn academy_preset_requires_one_explicit_pass_per_requirement() {
339        let preset = ProductionPreset::academy();
340        let passes = AcademyProductionRequirement::ALL
341            .iter()
342            .copied()
343            .map(|requirement| (requirement, AcademyCheckStatus::Pass));
344        assert_eq!(preset.validate(passes), Ok(()));
345
346        assert!(matches!(
347            preset.validate([
348                (
349                    AcademyProductionRequirement::AuthenticatedIdentity,
350                    AcademyCheckStatus::Pass,
351                ),
352                (
353                    AcademyProductionRequirement::AuthenticatedIdentity,
354                    AcademyCheckStatus::Fail,
355                ),
356            ]),
357            Err(AcademyPresetError::DuplicateRequirement(
358                AcademyProductionRequirement::AuthenticatedIdentity
359            ))
360        ));
361        assert_eq!(AcademyCheckStatus::Skipped.as_str(), "SKIPPED");
362    }
363}