openjd_sessions/session_user.rs
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! Session user types for cross-user execution — mirrors Python `_session_user.py`.
6
7/// Trait for session user identity.
8pub trait SessionUser: Send + Sync + std::fmt::Debug {
9 fn user(&self) -> &str;
10 fn group(&self) -> &str;
11 fn is_process_user(&self) -> bool;
12 fn as_any(&self) -> &dyn std::any::Any;
13}
14
15// ---------------------------------------------------------------------------
16// POSIX
17// ---------------------------------------------------------------------------
18
19/// POSIX session user identity for cross-user execution via sudo.
20#[cfg(unix)]
21#[derive(Debug, Clone)]
22pub struct PosixSessionUser {
23 pub user: String,
24 pub group: String,
25}
26
27#[cfg(unix)]
28impl PosixSessionUser {
29 /// Create a new PosixSessionUser.
30 ///
31 /// If `group` is None, defaults to the current process's effective group.
32 pub fn new(user: &str, group: Option<&str>) -> Self {
33 let group = match group {
34 Some(g) => g.to_string(),
35 None => {
36 let egid = nix::unistd::getegid();
37 nix::unistd::Group::from_gid(egid)
38 .ok()
39 .flatten()
40 .map(|g| g.name)
41 .unwrap_or_else(|| egid.to_string())
42 }
43 };
44 Self {
45 user: user.to_string(),
46 group,
47 }
48 }
49}
50
51#[cfg(unix)]
52impl SessionUser for PosixSessionUser {
53 fn as_any(&self) -> &dyn std::any::Any {
54 self
55 }
56 fn user(&self) -> &str {
57 &self.user
58 }
59
60 fn group(&self) -> &str {
61 &self.group
62 }
63
64 fn is_process_user(&self) -> bool {
65 let euid = nix::unistd::geteuid();
66 nix::unistd::User::from_uid(euid)
67 .ok()
68 .flatten()
69 .map(|u| u.name == self.user)
70 .unwrap_or(false)
71 }
72}
73
74// ---------------------------------------------------------------------------
75// Windows
76// ---------------------------------------------------------------------------
77
78/// Error for incorrect username or password.
79#[cfg(windows)]
80#[derive(Debug, thiserror::Error)]
81pub enum BadCredentialsError {
82 #[error("The username or password is incorrect.")]
83 LogonFailure,
84 #[error("{0}")]
85 Other(String),
86}
87
88/// Windows session user identity for cross-user execution.
89///
90/// Mirrors Python `WindowsSessionUser`. Two authentication modes:
91///
92/// - **Password mode** (non-Session 0): provide `user` + `password`.
93/// Credentials are validated immediately via `LogonUserW`.
94/// - **Logon token mode** (Session 0 / services / SSH): provide `user` + `logon_token`.
95///
96/// If the user is the same as the process owner, neither password nor token is needed.
97#[cfg(windows)]
98#[derive(Debug)]
99pub struct WindowsSessionUser {
100 user: String,
101 password: Option<String>,
102 logon_token: Option<windows::Win32::Foundation::HANDLE>,
103}
104
105// SAFETY: `WindowsSessionUser` is Send because all of its fields can be
106// sent across threads:
107// - `user: String` and `password: Option<String>` are Send by virtue of
108// being owned `String`s.
109// - `logon_token: Option<HANDLE>` is a Windows kernel object handle,
110// represented as a pointer-sized integer. Kernel handles are process-
111// wide and safe to use from any thread. The `HANDLE` type is marked
112// `!Send` in `windows-rs` out of caution because many Win32 APIs expect
113// the handle to remain associated with the original thread, but that is
114// not the case for the logon token here — it is only read and passed to
115// APIs that accept any thread's handle.
116#[cfg(windows)]
117unsafe impl Send for WindowsSessionUser {}
118// SAFETY: `WindowsSessionUser` is Sync because all fields are immutable
119// after construction (no interior mutability), so `&WindowsSessionUser`
120// can be shared across threads without data races. The `HANDLE` is only
121// read through `&self` accessors.
122#[cfg(windows)]
123unsafe impl Sync for WindowsSessionUser {}
124
125#[cfg(windows)]
126impl WindowsSessionUser {
127 /// Create a WindowsSessionUser for the current process user (no credentials needed).
128 pub fn for_process_user() -> Result<Self, String> {
129 let user = crate::win32::get_process_user()
130 .map_err(|e| format!("Failed to get process user: {e}"))?;
131 Ok(Self {
132 user,
133 password: None,
134 logon_token: None,
135 })
136 }
137
138 /// Create a WindowsSessionUser with a password (non-Session 0 only).
139 ///
140 /// Validates the credentials immediately via `LogonUserW`.
141 pub fn with_password(user: &str, password: &str) -> Result<Self, BadCredentialsError> {
142 if crate::win32::is_session_zero() {
143 return Err(BadCredentialsError::Other(
144 "Must supply a logon_token rather than a password. \
145 Passwords are not supported when running in Windows Session 0."
146 .into(),
147 ));
148 }
149
150 if let Ok(proc_user) = crate::win32::get_process_user() {
151 if user.eq_ignore_ascii_case(&proc_user) {
152 return Err(BadCredentialsError::Other(
153 "User is the process owner. Do not provide a password.".into(),
154 ));
155 }
156 }
157
158 Self::validate_credentials(user, password)?;
159
160 Ok(Self {
161 user: user.to_string(),
162 password: Some(password.to_string()),
163 logon_token: None,
164 })
165 }
166
167 /// Create a WindowsSessionUser with a pre-existing logon token (Session 0 / services).
168 ///
169 /// The caller is responsible for the lifetime of the token handle — it must
170 /// remain valid for the lifetime of this `WindowsSessionUser`.
171 pub fn with_logon_token(
172 user: &str,
173 token: windows::Win32::Foundation::HANDLE,
174 ) -> Result<Self, String> {
175 if let Ok(proc_user) = crate::win32::get_process_user() {
176 if user.eq_ignore_ascii_case(&proc_user) {
177 return Err("User is the process owner. Do not provide a logon token.".into());
178 }
179 }
180
181 Ok(Self {
182 user: user.to_string(),
183 password: None,
184 logon_token: Some(token),
185 })
186 }
187
188 /// Get the password, if this user was created with one.
189 pub fn password(&self) -> Option<&str> {
190 self.password.as_deref()
191 }
192
193 /// Get the logon token, if this user was created with one.
194 pub fn logon_token(&self) -> Option<windows::Win32::Foundation::HANDLE> {
195 self.logon_token
196 }
197
198 fn validate_credentials(user: &str, password: &str) -> Result<(), BadCredentialsError> {
199 match crate::win32::logon_user(user, password) {
200 Ok(_token) => Ok(()), // token dropped here, closing the handle
201 Err(e) => {
202 // ERROR_LOGON_FAILURE = 0x8007052E
203 let code = e.code().0 as u32;
204 if code == 0x8007052E {
205 Err(BadCredentialsError::LogonFailure)
206 } else {
207 Err(BadCredentialsError::Other(e.to_string()))
208 }
209 }
210 }
211 }
212}
213
214#[cfg(windows)]
215impl SessionUser for WindowsSessionUser {
216 fn as_any(&self) -> &dyn std::any::Any {
217 self
218 }
219 fn user(&self) -> &str {
220 &self.user
221 }
222
223 fn group(&self) -> &str {
224 ""
225 }
226
227 fn is_process_user(&self) -> bool {
228 crate::win32::get_process_user()
229 .map(|proc_user| self.user.eq_ignore_ascii_case(&proc_user))
230 .unwrap_or(false)
231 }
232}
233
234// ---------------------------------------------------------------------------
235// Tests
236// ---------------------------------------------------------------------------
237
238#[cfg(all(test, windows))]
239mod tests_windows {
240 use super::*;
241
242 /// `WindowsSessionUser::with_password` must reject the process user with
243 /// `BadCredentialsError::Other` (a structural rejection — credentials are
244 /// not validated against `LogonUserW` in this case). Callers that supply
245 /// the process user should use `WindowsSessionUser::for_process_user()`
246 /// instead.
247 ///
248 /// This exercises the `Other` branch of the error mapping that the Python
249 /// binding layer surfaces as `RuntimeError`. Pairs with
250 /// `tests/integration/test_cross_user_windows.rs::test_with_password_logon_failure_*`
251 /// which exercises the `LogonFailure` branch.
252 #[test]
253 fn with_password_process_owner_returns_other_variant() {
254 // GIVEN the user name of the current process
255 let proc_user = match crate::win32::get_process_user() {
256 Ok(u) => u,
257 // If we can't determine the process user we cannot drive this
258 // test — but that's a separate failure mode; bail out cleanly so
259 // CI doesn't false-fail on unrelated configuration issues.
260 Err(_) => return,
261 };
262
263 // WHEN with_password is called for that user
264 let result = WindowsSessionUser::with_password(&proc_user, "irrelevant");
265
266 // THEN we get Other, not LogonFailure — and the message clearly
267 // names the reason so callers can route it to a useful error path.
268 match result {
269 Err(BadCredentialsError::Other(msg)) => {
270 assert!(
271 msg.contains("process owner"),
272 "expected 'process owner' in message, got: {msg}",
273 );
274 }
275 Err(BadCredentialsError::LogonFailure) => {
276 panic!(
277 "process-owner rejection should be Other, not LogonFailure. \
278 The `Other` variant carries the structural-rejection message \
279 that callers depend on to distinguish this case from a real \
280 credential mismatch."
281 );
282 }
283 Ok(_) => panic!("with_password(process_user, ...) must reject"),
284 }
285 }
286}