From fdfcb1f033521684bef537c994dc7717b8c6f183 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:31:50 +0000 Subject: [PATCH] Automate user permissions and PUID/PGID configuration - Implements `ensure_media_user` in `system.rs` to detect SUDO_USER or create `server_manager` user. - Adds `user_id` and `group_id` to `HardwareInfo`. - Updates all services to use detected UID/GID for PUID/PGID env vars. - Adds `chown_recursive` to set permissions on install directory. - Updates tests to reflect changes. --- server_manager/src/core/hardware.rs | 4 ++ server_manager/src/core/system.rs | 59 ++++++++++++++++++++++- server_manager/src/main.rs | 10 +++- server_manager/src/services/apps.rs | 6 +-- server_manager/src/services/arr.rs | 4 +- server_manager/src/services/download.rs | 6 +-- server_manager/src/services/infra.rs | 6 +-- server_manager/src/services/media.rs | 4 +- server_manager/tests/integration_tests.rs | 6 +++ 9 files changed, 90 insertions(+), 15 deletions(-) diff --git a/server_manager/src/core/hardware.rs b/server_manager/src/core/hardware.rs index f74ba34..8a86ce7 100644 --- a/server_manager/src/core/hardware.rs +++ b/server_manager/src/core/hardware.rs @@ -19,6 +19,8 @@ pub struct HardwareInfo { pub has_intel_quicksync: bool, pub disk_gb: u64, pub swap_gb: u64, + pub user_id: u32, + pub group_id: u32, } impl HardwareInfo { @@ -58,6 +60,8 @@ impl HardwareInfo { has_intel_quicksync, disk_gb, swap_gb, + user_id: 1000, + group_id: 1000, } } diff --git a/server_manager/src/core/system.rs b/server_manager/src/core/system.rs index c81ad38..cc7a8cd 100644 --- a/server_manager/src/core/system.rs +++ b/server_manager/src/core/system.rs @@ -1,7 +1,8 @@ -use nix::unistd::Uid; +use nix::unistd::{Uid, User}; use std::process::Command; use std::path::Path; use std::fs; +use std::env; use anyhow::{Result, Context, bail}; use log::{info, warn}; @@ -51,6 +52,62 @@ pub fn install_dependencies() -> Result<()> { Ok(()) } +pub fn ensure_media_user() -> Result<(u32, u32)> { + // 1. Check if running under sudo (use the real user) + if let Ok(sudo_user) = env::var("SUDO_USER") { + if !sudo_user.is_empty() { + info!("Detected sudo user: {}", sudo_user); + if let Ok(Some(user)) = User::from_name(&sudo_user) { + info!("Using detected user: {} (UID: {}, GID: {})", sudo_user, user.uid, user.gid); + return Ok((user.uid.as_raw(), user.gid.as_raw())); + } + } + } + + // 2. Check if 'server_manager' user exists + if let Ok(Some(user)) = User::from_name("server_manager") { + info!("Found existing system user: server_manager (UID: {})", user.uid); + return Ok((user.uid.as_raw(), user.gid.as_raw())); + } + + // 3. Create 'server_manager' user + info!("Creating system user: server_manager"); + let status = Command::new("useradd") + .arg("-r") // System account + .arg("-m") // Create home directory (useful for some configs) + .arg("-d").arg("/opt/server_manager") // Home is install dir + .arg("-s").arg("/bin/bash") // Shell allowed for debugging + .arg("server_manager") + .status() + .context("Failed to create user server_manager")?; + + if !status.success() { + bail!("Failed to execute useradd command"); + } + + // Verify creation + if let Ok(Some(user)) = User::from_name("server_manager") { + Ok((user.uid.as_raw(), user.gid.as_raw())) + } else { + bail!("User server_manager was created but could not be found") + } +} + +pub fn chown_recursive(path: &Path, uid: u32, gid: u32) -> Result<()> { + info!("Recursively setting ownership of {:?} to {}:{}", path, uid, gid); + let status = Command::new("chown") + .arg("-R") + .arg(format!("{}:{}", uid, gid)) + .arg(path) + .status() + .context("Failed to execute chown")?; + + if !status.success() { + warn!("chown command returned non-zero exit code"); + } + Ok(()) +} + pub fn apply_optimizations() -> Result<()> { info!("Applying system optimizations for media server performance..."); diff --git a/server_manager/src/main.rs b/server_manager/src/main.rs index c92dc2f..579b982 100644 --- a/server_manager/src/main.rs +++ b/server_manager/src/main.rs @@ -48,6 +48,9 @@ async fn run_install() -> Result<()> { // 1. Root Check system::check_root()?; + // 1.0 Ensure Media User + let (uid, gid) = system::ensure_media_user()?; + // 1.1 Create Install Directory let install_dir = std::path::Path::new("/opt/server_manager"); if !install_dir.exists() { @@ -60,7 +63,9 @@ async fn run_install() -> Result<()> { let secrets = secrets::Secrets::load_or_create()?; // 2. Hardware Detection - let hw = hardware::HardwareInfo::detect(); + let mut hw = hardware::HardwareInfo::detect(); + hw.user_id = uid; + hw.group_id = gid; // 3. System Dependencies & Optimization system::install_dependencies()?; @@ -79,6 +84,9 @@ async fn run_install() -> Result<()> { // 7. Generate Compose generate_compose(&hw, &secrets).await?; + // 7.1 Set Permissions + system::chown_recursive(install_dir, uid, gid)?; + // 8. Launch info!("Launching Services via Docker Compose..."); let status = Command::new("docker") diff --git a/server_manager/src/services/apps.rs b/server_manager/src/services/apps.rs index 434e0c4..412c73e 100644 --- a/server_manager/src/services/apps.rs +++ b/server_manager/src/services/apps.rs @@ -140,10 +140,10 @@ $AUTOCONFIG = array( Ok(()) } - fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap { + fn env_vars(&self, hw: &HardwareInfo, secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); - vars.insert("PUID".to_string(), "1000".to_string()); - vars.insert("PGID".to_string(), "1000".to_string()); + vars.insert("PUID".to_string(), hw.user_id.to_string()); + vars.insert("PGID".to_string(), hw.group_id.to_string()); vars.insert("MYSQL_HOST".to_string(), "mariadb".to_string()); vars.insert("MYSQL_DATABASE".to_string(), "nextcloud".to_string()); vars.insert("MYSQL_USER".to_string(), "nextcloud".to_string()); diff --git a/server_manager/src/services/arr.rs b/server_manager/src/services/arr.rs index 030ba39..015cca9 100644 --- a/server_manager/src/services/arr.rs +++ b/server_manager/src/services/arr.rs @@ -18,8 +18,8 @@ macro_rules! define_arr_service { } fn env_vars(&self, hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); - vars.insert("PUID".to_string(), "1000".to_string()); - vars.insert("PGID".to_string(), "1000".to_string()); + vars.insert("PUID".to_string(), hw.user_id.to_string()); + vars.insert("PGID".to_string(), hw.group_id.to_string()); vars.insert("COMPlus_EnableDiagnostics".to_string(), "0".to_string()); if let HardwareProfile::Low = hw.profile { diff --git a/server_manager/src/services/download.rs b/server_manager/src/services/download.rs index 1b1ee6e..3e1c287 100644 --- a/server_manager/src/services/download.rs +++ b/server_manager/src/services/download.rs @@ -13,10 +13,10 @@ impl Service for QBittorrentService { vec!["8080:8080".to_string(), "6881:6881".to_string(), "6881:6881/udp".to_string()] } - fn env_vars(&self, _hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { + fn env_vars(&self, hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); - vars.insert("PUID".to_string(), "1000".to_string()); - vars.insert("PGID".to_string(), "1000".to_string()); + vars.insert("PUID".to_string(), hw.user_id.to_string()); + vars.insert("PGID".to_string(), hw.group_id.to_string()); vars.insert("WEBUI_PORT".to_string(), "8080".to_string()); vars } diff --git a/server_manager/src/services/infra.rs b/server_manager/src/services/infra.rs index c90a49d..b4794d2 100644 --- a/server_manager/src/services/infra.rs +++ b/server_manager/src/services/infra.rs @@ -49,10 +49,10 @@ impl Service for MariaDBService { Ok(()) } - fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap { + fn env_vars(&self, hw: &HardwareInfo, secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); - vars.insert("PUID".to_string(), "1000".to_string()); - vars.insert("PGID".to_string(), "1000".to_string()); + vars.insert("PUID".to_string(), hw.user_id.to_string()); + vars.insert("PGID".to_string(), hw.group_id.to_string()); vars.insert("MYSQL_ROOT_PASSWORD".to_string(), secrets.mysql_root_password.clone().unwrap_or_default()); vars.insert("MYSQL_DATABASE".to_string(), "server_manager".to_string()); vars.insert("MYSQL_USER".to_string(), "server_manager".to_string()); diff --git a/server_manager/src/services/media.rs b/server_manager/src/services/media.rs index 16e7342..c3d21e3 100644 --- a/server_manager/src/services/media.rs +++ b/server_manager/src/services/media.rs @@ -15,8 +15,8 @@ impl Service for PlexService { fn env_vars(&self, hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); - vars.insert("PUID".to_string(), "1000".to_string()); - vars.insert("PGID".to_string(), "1000".to_string()); + vars.insert("PUID".to_string(), hw.user_id.to_string()); + vars.insert("PGID".to_string(), hw.group_id.to_string()); vars.insert("VERSION".to_string(), "docker".to_string()); // Legacy requirement diff --git a/server_manager/tests/integration_tests.rs b/server_manager/tests/integration_tests.rs index d3e4578..02214e9 100644 --- a/server_manager/tests/integration_tests.rs +++ b/server_manager/tests/integration_tests.rs @@ -13,6 +13,8 @@ fn test_generate_compose_structure() { has_intel_quicksync: false, disk_gb: 512, swap_gb: 2, + user_id: 1000, + group_id: 1000, }; let secrets = Secrets { mysql_root_password: Some("rootpass".to_string()), @@ -65,6 +67,8 @@ fn test_profile_logic_low() { has_intel_quicksync: false, disk_gb: 100, swap_gb: 0, + user_id: 1000, + group_id: 1000, }; let secrets = Secrets::default(); @@ -90,6 +94,8 @@ fn test_profile_logic_standard() { has_intel_quicksync: false, disk_gb: 512, swap_gb: 4, + user_id: 1000, + group_id: 1000, }; let secrets = Secrets::default();