Skip to main content

chronon_core/
actor_policy.rs

1//! Optional policy for validating opaque `actor_json` at enqueue / upsert time.
2//!
3//! Chronon stores actor JSON as opaque data and rebuilds script context via
4//! [`ContextFactory`](crate::ContextFactory). Hosts that map JSON to privileged
5//! identities (for example a `System` shape) should install an [`ActorJsonPolicy`]
6//! so untrusted HTTP paths cannot mint elevated actors.
7
8use serde_json::Value;
9
10use crate::error::{ChrononError, Result};
11
12/// Trust level for an enqueue / upsert call site.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum EnqueueTrust {
15    /// In-process / bootstrapped callers (may mint elevated actor shapes when policy allows).
16    Internal,
17    /// HTTP admin or other externally reachable surfaces.
18    External,
19}
20
21/// Validates `actor_json` before a job is persisted.
22///
23/// # Errors
24///
25/// Implementations return [`ChrononError::ParamError`] when the actor is rejected.
26pub trait ActorJsonPolicy: Send + Sync {
27    /// Validate actor JSON for the given trust level.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error when the actor must not be stored.
32    fn validate(&self, trust: EnqueueTrust, actor_json: &Value) -> Result<()>;
33}
34
35/// Rejects well-known System-shaped actors on [`EnqueueTrust::External`] paths.
36///
37/// Recognizes `{"System": ...}` object keys (case-sensitive), matching common UF actor JSON.
38#[derive(Debug, Default, Clone, Copy)]
39pub struct RejectExternalSystemActor;
40
41impl ActorJsonPolicy for RejectExternalSystemActor {
42    fn validate(&self, trust: EnqueueTrust, actor_json: &Value) -> Result<()> {
43        if trust == EnqueueTrust::External && actor_json.get("System").is_some() {
44            return Err(ChrononError::ParamError(
45                "external enqueue cannot use System-shaped actor_json".into(),
46            ));
47        }
48        Ok(())
49    }
50}
51
52/// Default HTTP / external upsert actor (non-System service marker).
53///
54/// Shape: `{"Service":{"name":"chronon_api"}}`. Hosts that elevate privileges from actor JSON
55/// must not treat this marker as System.
56#[must_use]
57pub fn default_http_enqueue_actor() -> Value {
58    serde_json::json!({"Service": {"name": "chronon_api"}})
59}
60
61/// True when `actor_json` uses the well-known System object key.
62#[must_use]
63pub fn is_system_shaped_actor(actor_json: &Value) -> bool {
64    actor_json.get("System").is_some()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn reject_external_system() {
73        let policy = RejectExternalSystemActor;
74        let system = serde_json::json!({"System": {"operation": "x"}});
75        assert!(policy
76            .validate(EnqueueTrust::External, &system)
77            .unwrap_err()
78            .to_string()
79            .contains("System"));
80        assert!(policy.validate(EnqueueTrust::Internal, &system).is_ok());
81    }
82
83    #[test]
84    fn allow_external_service() {
85        let policy = RejectExternalSystemActor;
86        let service = default_http_enqueue_actor();
87        assert!(!is_system_shaped_actor(&service));
88        assert!(policy.validate(EnqueueTrust::External, &service).is_ok());
89    }
90}