245 lines
8.9 KiB
Rust
245 lines
8.9 KiB
Rust
use workshop_engine::ExecutionContext;
|
|
use crate::prebake::config::Security;
|
|
use crate::prebake::error::PrebakeError;
|
|
use crate::prebake::stage::PrebakeStage;
|
|
use privdrop::PrivDrop;
|
|
use std::ffi::CString;
|
|
use std::mem::MaybeUninit;
|
|
use std::str::FromStr;
|
|
|
|
const ROOT_UID: u32 = 0;
|
|
|
|
/// Resolves a username to its corresponding UID (User ID).
|
|
///
|
|
/// This function performs a thread-safe lookup of user information using the
|
|
/// `getpwnam_r` system call, which is the reentrant version of `getpwnam`.
|
|
/// It first attempts to find the user in the system password database,
|
|
/// and if not found, falls back to parsing the input as a numeric UID.
|
|
///
|
|
/// # Security Model
|
|
///
|
|
/// This function is part of the privilege management system. It is used
|
|
/// to resolve usernames to UIDs before privilege dropping operations,
|
|
/// ensuring that processes can transition to the correct user identity.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `username` - The username to look up. Can be either:
|
|
/// - A textual username (e.g., `"vulcan"`, `"nobody"`)
|
|
/// - A numeric UID string (e.g., `"1000"`) for fallback resolution
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns `Result<u32, PrebakeError>`:
|
|
/// * `Ok(u32)` - The resolved UID on success
|
|
/// * `Err(PrebakeError::UserNotFound)` - User does not exist in the system
|
|
/// database and is not a valid numeric UID
|
|
///
|
|
/// # Error Handling
|
|
///
|
|
/// This function handles several error cases:
|
|
/// 1. **Invalid username format**: If the username contains a null byte,
|
|
/// `CString::new` will fail, returning `UserNotFound`.
|
|
/// 2. **Buffer too small**: If `getpwnam_r` returns `ERANGE`, the buffer
|
|
/// is dynamically resized and the lookup is retried.
|
|
/// 3. **User not found**: If `getpwnam_r` returns `0` with a null pointer,
|
|
/// the function attempts to parse `username` as a numeric UID.
|
|
/// If that also fails, `UserNotFound` is returned.
|
|
///
|
|
/// # Thread Safety
|
|
///
|
|
/// This function is thread-safe because it uses `getpwnam_r` instead of
|
|
/// the non-reentrant `getpwnam`. The `getpwnam_r` function stores its
|
|
/// result in the caller-provided buffer, avoiding race conditions in
|
|
/// multi-threaded contexts.
|
|
///
|
|
/// # Implementation Details
|
|
///
|
|
/// The function uses a dynamically growing buffer strategy:
|
|
/// 1. Start with a 4KB buffer (`bufsize = 4096`)
|
|
/// 2. If `getpwnam_r` returns `ERANGE` (buffer too small), double the buffer
|
|
/// 3. Retry until the buffer is large enough or a non-ERANGE error occurs
|
|
///
|
|
/// If the user is not found in the system database, the function attempts
|
|
/// to parse the `username` string directly as a numeric UID. This allows
|
|
/// configurations to specify UIDs directly (e.g., `"0"` for root) without
|
|
/// relying on the system's user database.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// // Resolve a username to UID
|
|
/// match lookup_uid_by_name("vulcan") {
|
|
/// Ok(uid) => println!("User vulcan has UID {}", uid),
|
|
/// Err(e) => eprintln!("Failed to resolve user: {}", e),
|
|
/// }
|
|
///
|
|
/// // Resolve a numeric UID string (fallback)
|
|
/// match lookup_uid_by_name("1000") {
|
|
/// Ok(uid) => println!("UID 1000 resolved to {}", uid),
|
|
/// Err(e) => eprintln!("Invalid UID: {}", e),
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// # See Also
|
|
///
|
|
/// * [`prebake_drop_privilege`] - Uses this function to resolve target user before dropping privileges
|
|
/// * [`PrivDrop`](privdrop::PrivDrop) - The privilege dropping utility used after UID resolution
|
|
pub fn lookup_uid_by_name(username: &str) -> Result<u32, PrebakeError> {
|
|
let c_username = CString::new(username)
|
|
.map_err(|_| PrebakeError::UserNotFound(format!("Invalid username: {}", username)))?;
|
|
|
|
let mut pwd = MaybeUninit::<libc::passwd>::uninit();
|
|
let mut pwent = std::ptr::null_mut::<libc::passwd>();
|
|
let mut bufsize: usize = 4096;
|
|
let mut pwbuf = vec![0; bufsize];
|
|
|
|
loop {
|
|
let ret = unsafe {
|
|
libc::getpwnam_r(
|
|
c_username.as_ptr(),
|
|
pwd.as_mut_ptr(),
|
|
pwbuf.as_mut_ptr() as *mut libc::c_char,
|
|
pwbuf.len(),
|
|
&mut pwent,
|
|
)
|
|
};
|
|
|
|
if ret == libc::ERANGE {
|
|
bufsize *= 2;
|
|
pwbuf.resize(bufsize, 0);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// NOTE: originally unsafe {pwent} here
|
|
// let pwent = pwent ;
|
|
if pwent.is_null() {
|
|
if let Ok(uid) = username.parse::<u32>() {
|
|
return Ok(uid);
|
|
}
|
|
return Err(PrebakeError::UserNotFound(format!(
|
|
"User '{}' not found and not a valid numeric UID",
|
|
username
|
|
)));
|
|
}
|
|
|
|
let uid = unsafe { (*pwent).pw_uid };
|
|
Ok(uid)
|
|
}
|
|
|
|
/// Drops process privileges if the current execution stage exceeds the allowed privilege boundary.
|
|
///
|
|
/// # Security Model
|
|
/// This function acts as a **privilege gate**. It ensures that once the pipeline progresses beyond
|
|
/// a certain stage (`stage_expected`), the process must no longer run with elevated (Root) privileges.
|
|
///
|
|
/// # Behavior
|
|
/// - **Conditional Execution**: Only attempts to drop privileges if `stage_current > stage_expected`.
|
|
/// - **Idempotent**: Safe to call multiple times. If the process is already running as the target user,
|
|
/// it returns `Ok(())` immediately.
|
|
/// - **State Validation**:
|
|
/// - If running as Root: Attempts to switch to the target user.
|
|
/// - If running as Target User: Returns success (no-op).
|
|
/// - If running as Other Non-Root User: Returns `Err(InvalidPrivilegeState)` because privilege
|
|
/// dropping is impossible from this state (you cannot switch users without Root).
|
|
///
|
|
/// # Logic Flow
|
|
/// 1. Check if `stage_current` exceeds `stage_expected`. If not, return `Ok(())` immediately.
|
|
/// 2. Resolve the target username to a UID.
|
|
/// 3. Compare Current Effective UID vs. Target UID:
|
|
/// - **Equal**: Log info and return `Ok(())`.
|
|
/// - **Current is Root (0)**: Proceed to drop privileges using `PrivDrop`.
|
|
/// - **Current is Neither**: Log error and return `Err(InvalidPrivilegeState)`.
|
|
/// 4. Verify the new UID after dropping (sanity check) and log success.
|
|
///
|
|
/// # Arguments
|
|
/// * `stage_current`: The pipeline stage currently being executed.
|
|
/// * `stage_expected`: The last stage allowed to run with elevated privileges.
|
|
/// * `user`: The target username to drop privileges to (e.g., "nobody", "builder").
|
|
///
|
|
/// # Returns
|
|
/// - `Ok(())`: Privileges were successfully dropped, or were already correct.
|
|
/// - `Err(PrebakeError)`:
|
|
/// - `UserNotFound`: The target username does not exist.
|
|
/// - `InvalidPrivilegeState`: Process is running as an unexpected non-root user.
|
|
/// - `PrivDropFailed`: System call to change user failed.
|
|
///
|
|
/// # Caller Responsibility
|
|
/// The caller must ensure that if a privilege drop is required, this function is invoked
|
|
/// while the process still holds Root privileges. The caller is also responsible for mapping
|
|
/// the returned `Err` to an appropriate exit code (e.g., 201) if termination is desired.
|
|
pub fn prebake_drop_privilege(
|
|
stage_current: PrebakeStage,
|
|
stage_expected: PrebakeStage,
|
|
user: &str,
|
|
ctx: &mut ExecutionContext,
|
|
) -> Result<(), PrebakeError> {
|
|
if ctx.dry_run {
|
|
return Ok(());
|
|
}
|
|
if stage_current > stage_expected {
|
|
let current_uid = unsafe { libc::geteuid() };
|
|
|
|
let target_uid = lookup_uid_by_name(user).inspect_err(|_e| {
|
|
log::error!(
|
|
"Cannot drop privileges: target user '{}' does not exist at stage {}",
|
|
user,
|
|
stage_current
|
|
);
|
|
log::error!("Aborting pipeline.");
|
|
})?;
|
|
|
|
if current_uid == target_uid {
|
|
log::debug!(
|
|
"Already running as target user (UID {}), skipping privilege drop",
|
|
target_uid
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
if current_uid != ROOT_UID {
|
|
log::error!(
|
|
"Cannot drop privileges: current UID={} is neither Root (0) nor target UID={}",
|
|
current_uid,
|
|
target_uid
|
|
);
|
|
log::error!("Aborting pipeline.");
|
|
return Err(PrebakeError::InvalidPrivilegeState {
|
|
current_uid,
|
|
target_uid,
|
|
});
|
|
}
|
|
|
|
PrivDrop::default().user(user).apply().map_err(|e| {
|
|
log::error!(
|
|
"Failed to drop privileges to {} at stage {} (after {}): {}",
|
|
user,
|
|
stage_current,
|
|
stage_expected,
|
|
e
|
|
);
|
|
log::error!("Aborting pipeline.");
|
|
PrebakeError::PrivDropFailed(e.to_string())
|
|
})?;
|
|
|
|
let new_uid = unsafe { libc::geteuid() };
|
|
log::info!(
|
|
"Successfully dropped privileges from Root to UID {} (user: {})",
|
|
new_uid,
|
|
user
|
|
);
|
|
ctx.privileged = false;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_drop_after(security: &Option<Security>) -> Result<PrebakeStage, String> {
|
|
security
|
|
.clone()
|
|
.and_then(|sec| sec.drop_after)
|
|
.map(|stage_str| PrebakeStage::from_str(&stage_str))
|
|
.unwrap_or(Ok(PrebakeStage::default()))
|
|
}
|