Skip to main content

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}