Skip to main content

orcs_auth/
session.rs

1//! Session types (Principal + Privilege).
2
3use crate::PrivilegeLevel;
4use orcs_types::Principal;
5use std::time::Duration;
6
7/// An active security context combining identity and privilege.
8///
9/// A Session represents the current state of an actor in the system:
10///
11/// - **Who**: The [`Principal`] (user, component, or system)
12/// - **What level**: The [`PrivilegeLevel`] (standard or elevated)
13///
14/// # Immutability
15///
16/// Sessions are immutable value types. Methods like [`elevate`](Self::elevate)
17/// and [`drop_privilege`](Self::drop_privilege) return new sessions rather
18/// than modifying the existing one. This enables:
19///
20/// - Safe sharing across threads
21/// - Clear audit trails (old session vs new session)
22/// - Simple `Clone`
23///
24/// # Dynamic Permissions
25///
26/// Dynamic command permissions (grant/revoke) are managed separately
27/// via [`GrantPolicy`](crate::GrantPolicy), not by Session.
28/// Session only carries identity and privilege level.
29///
30/// # Why No Default?
31///
32/// **DO NOT implement `Default` for Session.**
33///
34/// A session requires a valid [`Principal`]. There is no sensible
35/// default identity. Always construct with [`Session::new`].
36///
37/// # Example
38///
39/// ```
40/// use orcs_auth::{Session, PrivilegeLevel};
41/// use orcs_types::{Principal, PrincipalId};
42/// use std::time::Duration;
43///
44/// // Create a session for a user
45/// let user = Principal::User(PrincipalId::new());
46/// let session = Session::new(user);
47///
48/// // Check current state
49/// assert!(!session.is_elevated());
50///
51/// // Elevate for privileged operations
52/// let elevated = session.elevate(Duration::from_secs(300));
53/// assert!(elevated.is_elevated());
54///
55/// // Drop back to standard when done
56/// let standard = elevated.drop_privilege();
57/// assert!(!standard.is_elevated());
58/// ```
59#[derive(Debug, Clone)]
60pub struct Session {
61    /// The actor performing operations.
62    principal: Principal,
63    /// Current privilege level.
64    privilege: PrivilegeLevel,
65}
66
67impl Session {
68    /// Creates a new session with Standard privilege level.
69    ///
70    /// All sessions start in Standard mode. Use [`elevate`](Self::elevate)
71    /// to gain elevated privileges.
72    ///
73    /// # Example
74    ///
75    /// ```
76    /// use orcs_auth::Session;
77    /// use orcs_types::{Principal, PrincipalId};
78    ///
79    /// let session = Session::new(Principal::User(PrincipalId::new()));
80    /// assert!(!session.is_elevated());
81    /// ```
82    #[must_use]
83    pub fn new(principal: Principal) -> Self {
84        Self {
85            principal,
86            privilege: PrivilegeLevel::Standard,
87        }
88    }
89
90    /// Returns a reference to the principal.
91    #[must_use]
92    pub fn principal(&self) -> &Principal {
93        &self.principal
94    }
95
96    /// Returns a reference to the current privilege level.
97    #[must_use]
98    pub fn privilege(&self) -> &PrivilegeLevel {
99        &self.privilege
100    }
101
102    /// Returns `true` if currently elevated (and not expired).
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// use orcs_auth::Session;
108    /// use orcs_types::{Principal, PrincipalId};
109    /// use std::time::Duration;
110    ///
111    /// let session = Session::new(Principal::User(PrincipalId::new()));
112    /// assert!(!session.is_elevated());
113    ///
114    /// let elevated = session.elevate(Duration::from_secs(60));
115    /// assert!(elevated.is_elevated());
116    /// ```
117    #[must_use]
118    pub fn is_elevated(&self) -> bool {
119        self.privilege.is_elevated()
120    }
121
122    /// Returns a new session with elevated privileges.
123    ///
124    /// The elevation lasts for the specified duration, after which
125    /// the session automatically behaves as Standard.
126    ///
127    /// # Arguments
128    ///
129    /// * `duration` - How long the elevation should last
130    ///
131    /// # Example
132    ///
133    /// ```
134    /// use orcs_auth::Session;
135    /// use orcs_types::{Principal, PrincipalId};
136    /// use std::time::Duration;
137    ///
138    /// let session = Session::new(Principal::User(PrincipalId::new()));
139    ///
140    /// // Elevate for 5 minutes
141    /// let elevated = session.elevate(Duration::from_secs(300));
142    /// assert!(elevated.is_elevated());
143    ///
144    /// // Original session is unchanged
145    /// assert!(!session.is_elevated());
146    /// ```
147    #[must_use]
148    pub fn elevate(&self, duration: Duration) -> Self {
149        Self {
150            principal: self.principal.clone(),
151            privilege: PrivilegeLevel::elevated_for(duration),
152        }
153    }
154
155    /// Returns a new session with Standard privilege level.
156    ///
157    /// Use this to explicitly drop elevated privileges before
158    /// the automatic expiration.
159    ///
160    /// # Example
161    ///
162    /// ```
163    /// use orcs_auth::Session;
164    /// use orcs_types::{Principal, PrincipalId};
165    /// use std::time::Duration;
166    ///
167    /// let session = Session::new(Principal::User(PrincipalId::new()));
168    /// let elevated = session.elevate(Duration::from_secs(300));
169    ///
170    /// // Explicitly drop privileges
171    /// let standard = elevated.drop_privilege();
172    /// assert!(!standard.is_elevated());
173    /// ```
174    #[must_use]
175    pub fn drop_privilege(&self) -> Self {
176        Self {
177            principal: self.principal.clone(),
178            privilege: PrivilegeLevel::Standard,
179        }
180    }
181
182    /// Returns the remaining elevation time, or `None` if not elevated.
183    #[must_use]
184    pub fn remaining_elevation(&self) -> Option<Duration> {
185        self.privilege.remaining()
186    }
187}
188
189impl std::fmt::Display for Session {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        let level = if self.is_elevated() {
192            "elevated"
193        } else {
194            "standard"
195        };
196        write!(f, "{}@{}", self.principal, level)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use orcs_types::PrincipalId;
204
205    #[test]
206    fn new_session_is_standard() {
207        let session = Session::new(Principal::User(PrincipalId::new()));
208        assert!(!session.is_elevated());
209        assert!(session.remaining_elevation().is_none());
210    }
211
212    #[test]
213    fn elevate_creates_new_session() {
214        let session = Session::new(Principal::User(PrincipalId::new()));
215        let elevated = session.elevate(Duration::from_secs(60));
216
217        // Original unchanged
218        assert!(!session.is_elevated());
219        // New session is elevated
220        assert!(elevated.is_elevated());
221    }
222
223    #[test]
224    fn drop_privilege_creates_new_session() {
225        let session = Session::new(Principal::User(PrincipalId::new()));
226        let elevated = session.elevate(Duration::from_secs(60));
227        let dropped = elevated.drop_privilege();
228
229        // Elevated session unchanged
230        assert!(elevated.is_elevated());
231        // Dropped session is standard
232        assert!(!dropped.is_elevated());
233    }
234
235    #[test]
236    fn principal_preserved_after_elevate() {
237        let id = PrincipalId::new();
238        let session = Session::new(Principal::User(id));
239        let elevated = session.elevate(Duration::from_secs(60));
240
241        assert_eq!(
242            session.principal().user_id(),
243            elevated.principal().user_id()
244        );
245    }
246
247    #[test]
248    fn display_shows_level() {
249        let session = Session::new(Principal::User(PrincipalId::new()));
250        let display = format!("{session}");
251        assert!(display.contains("standard"));
252
253        let elevated = session.elevate(Duration::from_secs(60));
254        let display = format!("{elevated}");
255        assert!(display.contains("elevated"));
256    }
257
258    #[test]
259    fn system_session() {
260        let session = Session::new(Principal::System);
261        assert!(session.principal().is_system());
262        assert!(!session.is_elevated());
263    }
264
265    #[test]
266    fn clone_preserves_all_fields() {
267        let session = Session::new(Principal::User(PrincipalId::new()));
268        let elevated = session.elevate(Duration::from_secs(60));
269        let cloned = elevated.clone();
270
271        assert!(cloned.is_elevated());
272        assert_eq!(elevated.principal().user_id(), cloned.principal().user_id());
273    }
274}