Skip to main content

orcs_auth/
privilege.rs

1//! Privilege level types.
2
3use std::time::{Duration, Instant};
4
5/// The current privilege level of a session.
6///
7/// This implements a sudo-like model where all actors start with
8/// limited permissions and must explicitly elevate to perform
9/// privileged operations.
10///
11/// # Design Rationale
12///
13/// ## Why Not Always Elevated?
14///
15/// Even human users operate in Standard mode by default:
16///
17/// - **Prevents accidents**: `git reset --hard` requires explicit elevation
18/// - **Audit clarity**: Elevated actions are intentional and logged
19/// - **Network safety**: Compromised sessions have limited damage potential
20///
21/// ## Time-Limited Elevation
22///
23/// Elevated privileges automatically expire to minimize the window
24/// of elevated access. This follows the principle of least privilege.
25///
26/// # Example
27///
28/// ```
29/// use orcs_auth::PrivilegeLevel;
30/// use std::time::{Duration, Instant};
31///
32/// // Standard mode (default)
33/// let standard = PrivilegeLevel::Standard;
34/// assert!(!standard.is_elevated());
35///
36/// // Elevated mode (explicit, time-limited)
37/// let until = Instant::now() + Duration::from_secs(300);
38/// let elevated = PrivilegeLevel::Elevated { until };
39/// assert!(elevated.is_elevated());
40/// ```
41#[derive(Debug, Clone, Default)]
42pub enum PrivilegeLevel {
43    /// Normal operations only.
44    ///
45    /// In this mode, the following are **not allowed**:
46    ///
47    /// - Global signals (Veto)
48    /// - Destructive file operations (`rm -rf`, overwrite without backup)
49    /// - Destructive git operations (`reset --hard`, `push --force`)
50    /// - Modifying system configuration
51    ///
52    /// This is the default mode for all principals.
53    #[default]
54    Standard,
55
56    /// Elevated privileges with expiration.
57    ///
58    /// Grants full access to all operations until the specified time.
59    /// After expiration, the session automatically drops to Standard.
60    ///
61    /// # Fields
62    ///
63    /// * `until` - When elevation expires (automatically serializes as duration from now)
64    Elevated {
65        /// Expiration time for elevated privileges.
66        until: Instant,
67    },
68}
69
70impl PrivilegeLevel {
71    /// Creates a new Standard privilege level.
72    #[must_use]
73    pub fn standard() -> Self {
74        Self::Standard
75    }
76
77    /// Creates a new Elevated privilege level with the given duration.
78    ///
79    /// # Example
80    ///
81    /// ```
82    /// use orcs_auth::PrivilegeLevel;
83    /// use std::time::Duration;
84    ///
85    /// let elevated = PrivilegeLevel::elevated_for(Duration::from_secs(60));
86    /// assert!(elevated.is_elevated());
87    /// ```
88    #[must_use]
89    pub fn elevated_for(duration: Duration) -> Self {
90        Self::Elevated {
91            until: Instant::now() + duration,
92        }
93    }
94
95    /// Returns `true` if currently elevated (and not expired).
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use orcs_auth::PrivilegeLevel;
101    /// use std::time::Duration;
102    ///
103    /// let standard = PrivilegeLevel::Standard;
104    /// assert!(!standard.is_elevated());
105    ///
106    /// let elevated = PrivilegeLevel::elevated_for(Duration::from_secs(60));
107    /// assert!(elevated.is_elevated());
108    /// ```
109    #[must_use]
110    pub fn is_elevated(&self) -> bool {
111        match self {
112            Self::Standard => false,
113            Self::Elevated { until } => Instant::now() < *until,
114        }
115    }
116
117    /// Returns `true` if this is Standard mode or elevation has expired.
118    #[must_use]
119    pub fn is_standard(&self) -> bool {
120        !self.is_elevated()
121    }
122
123    /// Returns the remaining elevation time, or `None` if not elevated.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use orcs_auth::PrivilegeLevel;
129    /// use std::time::Duration;
130    ///
131    /// let elevated = PrivilegeLevel::elevated_for(Duration::from_secs(60));
132    /// let remaining = elevated.remaining();
133    /// assert!(remaining.is_some());
134    /// assert!(remaining.unwrap() <= Duration::from_secs(60));
135    /// ```
136    #[must_use]
137    pub fn remaining(&self) -> Option<Duration> {
138        match self {
139            Self::Standard => None,
140            Self::Elevated { until } => {
141                let now = Instant::now();
142                if now < *until {
143                    Some(*until - now)
144                } else {
145                    None
146                }
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn standard_is_not_elevated() {
158        let level = PrivilegeLevel::Standard;
159        assert!(!level.is_elevated());
160        assert!(level.is_standard());
161        assert!(level.remaining().is_none());
162    }
163
164    #[test]
165    fn elevated_is_elevated() {
166        let level = PrivilegeLevel::elevated_for(Duration::from_secs(60));
167        assert!(level.is_elevated());
168        assert!(!level.is_standard());
169        assert!(level.remaining().is_some());
170    }
171
172    #[test]
173    fn expired_elevation_is_standard() {
174        let level = PrivilegeLevel::Elevated {
175            until: Instant::now() - Duration::from_secs(1),
176        };
177        assert!(!level.is_elevated());
178        assert!(level.is_standard());
179        assert!(level.remaining().is_none());
180    }
181
182    #[test]
183    fn default_is_standard() {
184        let level = PrivilegeLevel::default();
185        assert!(level.is_standard());
186    }
187
188    #[test]
189    fn remaining_decreases() {
190        let level = PrivilegeLevel::elevated_for(Duration::from_secs(60));
191        let remaining = level
192            .remaining()
193            .expect("elevated level should have remaining duration");
194        assert!(remaining <= Duration::from_secs(60));
195    }
196}