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}