Skip to main content

aperion_shield/orgmode/
mod.rs

1//! Aperion Shield -- org-mode client.
2//!
3//! When `aperion-shield enroll <token>` is run, this module bootstraps a
4//! persistent record at `~/.aperion-shield/orgmode.json` and switches the
5//! binary into "org mode":
6//!
7//!   - Policy is pulled from Smartflow every 30 s, hot-reloaded on
8//!     version bump.
9//!   - Audit events are streamed to `/api/enterprise/shield/events`.
10//!   - Identity-gated rules use the `SmartflowProvider`, which delegates
11//!     to Smartflow as the relying party (which in turn talks to ID.me
12//!     or any configured OIDC IdP).
13//!   - A heartbeat keeps the fleet dashboard accurate.
14//!
15//! When no enrollment record exists, none of this code runs and the
16//! binary behaves byte-identically to the free standalone tier. The
17//! org-mode plumbing is intentionally optional and additive.
18//!
19//! Module layout:
20//!
21//! ```text
22//! orgmode/
23//!   mod.rs               -- public OrgClient + top-level coordinator
24//!   state.rs             -- on-disk enrollment record
25//!   client.rs            -- thin reqwest wrapper around the smartflow REST API
26//!   enroll.rs            -- `aperion-shield enroll <token>` impl
27//!   heartbeat.rs         -- 30 s heartbeat task
28//!   policy_pull.rs       -- 30 s policy version poll + hot-reload publisher
29//!   audit_sink.rs        -- bounded queue, batched POST every 5 s
30//!   smartflow_provider.rs -- IdentityProvider implementation
31//! ```
32
33#![allow(clippy::module_inception)]
34
35pub mod audit_sink;
36pub mod client;
37pub mod enroll;
38pub mod heartbeat;
39pub mod policy_pull;
40pub mod smartflow_provider;
41pub mod state;
42
43pub use audit_sink::{AuditEvent, AuditSink};
44pub use client::{OrgApi, OrgApiError};
45pub use enroll::{run_disenroll, run_enroll, run_status};
46pub use heartbeat::start_heartbeat;
47pub use policy_pull::{start_policy_pull, PolicyPullHandle};
48pub use smartflow_provider::SmartflowProvider;
49pub use state::{OrgState, ORG_STATE_FILE};
50
51use std::sync::Arc;
52
53use crate::Engine;
54
55/// Outcome of attempting to bootstrap org mode at startup.
56pub enum OrgBootstrap {
57    /// No `orgmode.json` was found -- behave as plain standalone.
58    Standalone,
59    /// Enrolled with smartflow. The contained handles are the policy
60    /// pull / heartbeat / audit-sink machinery; dropping any of them
61    /// drains the corresponding task.
62    Enrolled(EnrolledHandles),
63}
64
65/// Handles returned when org mode is active. Held by `main()` for the
66/// lifetime of the process.
67pub struct EnrolledHandles {
68    pub state: OrgState,
69    pub api: Arc<OrgApi>,
70    pub policy: PolicyPullHandle,
71    pub audit: Arc<AuditSink>,
72    pub _heartbeat_task: tokio::task::JoinHandle<()>,
73}
74
75/// Helper used by `main()` to load the engine that should be active at
76/// process start: org-mode shieldset if enrolled (and reachable), local
77/// rules otherwise.
78///
79/// On failure to reach Smartflow we fall back to the local engine and
80/// log a warning -- this matches the `cached_policy` default offline
81/// behaviour documented in the strategy memo.
82pub async fn load_initial_engine(
83    state: &OrgState,
84    api: &OrgApi,
85    fallback: Engine,
86) -> Engine {
87    match api.get_shieldset(&state.policy_group).await {
88        Ok((yaml, version)) => {
89            log::warn!(
90                "[shield] org-mode policy pulled from {} group={} version={}",
91                state.smartflow_url,
92                state.policy_group,
93                version
94            );
95            match crate::Engine::from_yaml(&yaml) {
96                Ok(eng) => eng,
97                Err(e) => {
98                    log::error!(
99                        "[shield] failed to compile pulled shieldset (group={}): {}. \
100                         Falling back to local rules.",
101                        state.policy_group,
102                        e
103                    );
104                    fallback
105                }
106            }
107        }
108        Err(e) => {
109            log::warn!(
110                "[shield] could not pull policy from Smartflow ({}); using local rules: {}",
111                state.smartflow_url,
112                e
113            );
114            fallback
115        }
116    }
117}