From 7583deeb42e8c85ff91206e952e4fa06b41ea2dc Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Thu, 13 May 2021 01:16:14 +0200 Subject: [PATCH] Cache all user information as early as possible and re-use without lookup if needed --- src/cache.rs | 269 ++++++++++++++++++++++++++++++++------------------- src/nss.rs | 69 ++++--------- src/pam.rs | 3 +- 3 files changed, 194 insertions(+), 147 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index f254d50..a7db22f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -15,11 +15,10 @@ */ use crate::BASE_NAME; -use crate::unix::getpwnam_safe; +use crate::unix::{Passwd, getpwnam_safe, getpwuid_safe}; use lazy_static::lazy_static; -use std::collections::HashMap; -use std::sync::{RwLock, RwLockReadGuard}; +use std::sync::{Mutex, MutexGuard}; use libc::{geteuid, seteuid, uid_t}; @@ -37,119 +36,211 @@ use serde_json; const USER_TOKEN_FILENAME: &str = "user_token.json"; +struct UserInfo { + uid: Option<uid_t>, + username: Option<String>, + passwd: Option<Passwd>, + access_token: Option<BasicTokenResponse> +} + pub struct Cache { - user_tokens: HashMap<String, BasicTokenResponse>, - original_euid: uid_t, + pub context_user: UserInfo } impl Cache { pub fn new() -> Cache { - let euid; - unsafe { - euid = geteuid(); + let euid = unsafe { + geteuid() }; Cache { - user_tokens: HashMap::new(), - original_euid: euid + context_user: UserInfo { + uid: None, + username: None, + passwd: None, + access_token: None + } } } +} + +impl UserInfo { + pub fn set_current_user(&mut self) { + self.set_uid(original_euid); + } + + pub fn is_initialized(&self) -> bool { + self.uid.is_some() || self.username.is_some() + } - fn drop_privileges(&self, username: &String) -> Result<uid_t, io::Error> { - let current_euid; - unsafe { - current_euid = geteuid(); + fn try_resolve(&mut self) -> Result<&Passwd, io::Error> { + // If we already have a full passwd struct, return it as without resolving + if self.passwd.is_some() { + debug!("passwd entry for context user already resolved"); + return Ok(self.passwd.as_ref().unwrap()); + } + + // If we cannot call getpwnam safely, return error (see `is_get_pwnam_safe`) + if !is_getpwnam_safe() { + let msg = "Context user cannot be resolved safely right now"; + warn!("{}", msg); + return Err(io::Error::new(io::ErrorKind::WouldBlock, msg)); + } + + // Try one of the partial information to resolve + let res = match self.uid { + Some(uid) => getpwuid_safe(uid), + None => match &self.username { + Some(username) => getpwnam_safe(username.to_string()), + None => { + let msg = "No partial information set to use for getpwnam / getpwuid"; + warn!("{}", msg); + Err(io::Error::new(io::ErrorKind::InvalidInput, msg)) + } + } }; + match res { + Ok(passwd) => { + debug!("Successfully resolved context user's passwd entry"); + self.passwd = Some(passwd); + Ok(self.passwd.as_ref().unwrap()) + }, + Err(e) => Err(e) + } + } + + pub fn get_uid(&mut self) -> Result<uid_t, io::Error> { + match self.try_resolve() { + Ok(passwd) => Ok(passwd.pw_uid), + Err(e) => match self.uid { + Some(uid) => Ok(uid), + None => Err(e) + } + } + } + + pub fn set_uid(&mut self, uid: uid_t) { + self.uid = Some(uid); + self.username = None; + self.passwd = None; + self.try_resolve(); + } - let pw = match getpwnam_safe(username.to_string()) { - Ok(p) => p, + pub fn get_username(&mut self) -> Result<String, io::Error> { + match self.try_resolve() { + Ok(passwd) => Ok(&passwd.pw_name), + Err(e) => match self.username { + Some(username) => Ok(username), + None => Err(e) + } + } + } + + pub fn set_username(&mut self, username: String) { + self.username = Some(username); + self.uid = None; + self.passwd = None; + self.try_resolve(); + } + + pub fn get_home_directory(&mut self) -> Result<String, io::Error> { + match self.try_resolve() { + Ok(passwd) => Ok(passwd.pw_dir), + Err(e) => Err(e) + } + } + + fn drop_privileges(&self) -> Result<uid_t, io::Error> { + let current_euid = unsafe { + geteuid() + }; + + let target_euid = match self.get_uid() { + Ok(uid) => uid, Err(e) => { - error!("Failed to lookup user {}: {}", username, e); + error!("Could not drop privileges because target UID is not resolved"); return Err(e); } }; - debug!("Lookup for user {} returned UID {}, GID {}", username, pw.pw_uid, pw.pw_gid); - let target_euid = pw.pw_uid; if target_euid == current_euid { - debug!("No need to drop privileges, already running as {}", username); + debug!("No need to drop privileges, already running as {}", current_euid); return Ok(current_euid); } - let res; - unsafe { - res = seteuid(target_euid); + let res = unsafe { + seteuid(target_euid) }; if res == 0 { - debug!("Successfully dropped privileges to {}", username); + debug!("Successfully dropped privileges to {}", target_euid); return Ok(target_euid); } else { let e = io::Error::last_os_error(); - error!("Could not drop privileges to {}", username); + error!("Could not drop privileges to {}", target_euid); return Err(e); } } fn restore_privileges(&self) { - let current_euid; - unsafe { - current_euid = geteuid(); + let current_euid = unsafe { + geteuid() }; - if current_euid != self.original_euid { + if current_euid != original_euid { debug!("Restoring privileges"); - let res; - unsafe { - res = seteuid(self.original_euid); + let res = unsafe { + seteuid(original_euid) }; if res != 0 { - panic!("Could not restore privileges to {}", self.original_euid); + panic!("Could not restore privileges to {}", original_euid); } } else { debug!("No need to restore privileges, already running as original user"); } } - fn get_user_xdg_base_directories(&self, username: &String) -> Result<BaseDirectories, io::Error> { + fn get_user_xdg_base_directories(&mut self) -> Result<BaseDirectories, io::Error> { + // Save original $HOME for later restore let saved_home = env::var_os("HOME"); - let pw = match getpwnam_safe(username.to_string()) { - Ok(p) => p, - Err(e) => { - error!("Failed to lookup user {}: {}", username, e); - return Err(e); - } - }; - debug!("Lookup for user {} returned UID {}, GID {}", username, pw.pw_uid, pw.pw_gid); - let user_home = pw.pw_dir; - - env::set_var("HOME", &user_home); - debug!("Home directory for {} is {}", username, user_home); + // Determine user ID to find out whether we should override $HOME + let uid = self.get_uid()?; + if uid != original_euid { + let user_home = self.get_home_directory()?; + env::set_var("HOME", user_home); + debug!("Home directory for UID {} is {}", uid, user_home); + } + // Determine XDG directories now let base_dirs = match BaseDirectories::with_prefix(BASE_NAME) { Ok(b) => b, Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)) }; - if saved_home != None { - env::set_var("HOME", saved_home.unwrap()); - } else { - env::remove_var("HOME"); + + // Restore $HOME to original if we changed it earlier + if uid != original_euid { + if saved_home != None { + env::set_var("HOME", saved_home.unwrap()); + } else { + env::remove_var("HOME"); + } } + return Ok(base_dirs); } - fn place_user_cache_file(&self, username: &String, filename: String) -> Result<PathBuf, io::Error> { - match self.get_user_xdg_base_directories(username) { + fn place_user_cache_file(&mut self, filename: String) -> Result<PathBuf, io::Error> { + match self.get_user_xdg_base_directories() { Ok(b) => b.place_cache_file(filename), - Err(e) => Err(io::Error::new(io::ErrorKind::NotFound, e)) + Err(e) => Err(e) } } - pub fn load_user_token(&self, owner: &String) -> Option<&BasicTokenResponse> { - if !self.user_tokens.contains_key(owner) { - debug!("No token for {} in memory, trying to load from file", owner); - - self.drop_privileges(owner).ok(); - let new_token = match self.place_user_cache_file(owner, USER_TOKEN_FILENAME.to_string()) { + pub fn get_access_token(&mut self) -> Option<BasicTokenResponse> { + // Try to load our acess token if none is known + if self.access_token.is_none() { + debug!("No token in memory, trying to load from file"); + self.drop_privileges().ok(); + self.access_token = match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) { Ok(path) => match load_json(path) { Ok(read_token) => Some(read_token), Err(_) => None @@ -157,55 +248,29 @@ impl Cache { Err(_) => None }; self.restore_privileges(); - - match new_token { - Some(t) => { - CACHE.write().unwrap().user_tokens.insert(owner.to_string(), t); - self.user_tokens.get(owner) - }, - None => None - } - } else { - debug!("Found token for {} in memory", owner); - self.user_tokens.get(owner) } + + return self.access_token; } - pub fn save_user_token(&self, owner: &String, token: BasicTokenResponse) -> Result<(), io::Error> { - CACHE.write().unwrap().user_tokens.insert(owner.to_string(), token.clone()); - debug!("Saved token for {} in memory", owner); + pub fn set_access_token(&mut self, token: BasicTokenResponse) -> Result<(), io::Error> { + self.access_token = Some(token.clone()); + debug!("Saved token for in memory"); // Try to write user's token cache file - let res = match self.drop_privileges(owner) { - Ok(_) => match self.place_user_cache_file(owner, USER_TOKEN_FILENAME.to_string()) { + let res = match self.drop_privileges() { + Ok(_) => match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) { Ok(path) => { - debug!("Storing token for {} in cache file", owner); + debug!("Storing token for in cache file"); save_json(path, token) }, Err(e) => Err(e) }, - Err(e) => Err(io::Error::new(io::ErrorKind::PermissionDenied, e)) + Err(e) => Err(e) }; self.restore_privileges(); return res; } - - pub fn delete_user_token(&self, owner: &String) { - CACHE.write().unwrap().user_tokens.remove(owner); - debug!("Token for {} removed from memory", owner); - - // Try to remove user's token cache file - self.drop_privileges(owner).ok(); - match self.place_user_cache_file(owner, USER_TOKEN_FILENAME.to_string()) { - Ok(path) => { - debug!("Deleting cache file for {}", owner); - fs::remove_file(path).ok(); - () - }, - Err(e) => () - }; - self.restore_privileges(); - } } fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> { @@ -226,10 +291,18 @@ fn save_json<O: Serialize>(path: PathBuf, obj: O) -> Result<(), io::Error> { fs::write(path, json) } -lazy_static! { - static ref CACHE: RwLock<Cache> = RwLock::new(Cache::new()); +fn is_getpwnam_safe() -> bool { + // FIXME Implement real logic + return true; } -pub fn get_cache() -> RwLockReadGuard<'static, Cache> { - CACHE.read().unwrap() +static original_euid: uid_t = unsafe { + geteuid() +}; + +lazy_static! { + static ref CACHE: Mutex<Cache> = Mutex::new(Cache::new()); +} +pub fn get_cache() -> MutexGuard<'static, Cache> { + CACHE.lock().unwrap() } diff --git a/src/nss.rs b/src/nss.rs index 402af6e..c8af522 100644 --- a/src/nss.rs +++ b/src/nss.rs @@ -16,7 +16,6 @@ use crate::config::{ get_config, get_optional, - get_or_error }; use config::Config; use crate::cache::get_cache; @@ -26,10 +25,6 @@ use crate::logging::setup_log; use crate::oauth::{get_access_token_client, get_data_jq}; use serde::{Serialize, Deserialize}; -use std::ffi::CStr; -use libc::geteuid; -use crate::unix::getpwuid_safe; - use libnss::interop::Response; use libnss::passwd::{PasswdHooks, Passwd}; @@ -58,40 +53,25 @@ fn nss_hook_prepare() -> Config { return conf; } -fn get_current_user() -> String { - let euid; - unsafe { - euid = geteuid(); - }; - match getpwuid_safe(euid) { - Ok(p) => p.pw_name, - Err(e) => { - error!("Failed to lookup username for UID {}: {}", euid, e); - /* FIXME: return something (empty string?) to make the caller bail out early */ - "nobody".to_string() - } - } -} - struct OidcPasswd; impl PasswdHooks for OidcPasswd { fn get_all_entries() -> Response<Vec<Passwd>> { let conf = nss_hook_prepare(); - let mut cache = get_cache(); - let user = get_current_user(); - let ctc; - let token = match cache.load_user_token(&user) { + // Set the context user to the current user, but only if not already set + // When doing PAM, we might get called back into by libc to do some NSS + // lookup, and we want to keep the PAM login user context in that case + if !get_cache().context_user.is_initialized() { + get_cache().context_user.set_current_user(); + } + let token = match get_cache().context_user.get_access_token() { Some(t) => t, None => { // FIXME Implement caching of system token - debug!("Could not find a user token for {} to request NSS data; trying client credentials", user); + debug!("Could not find a user token to request NSS data; trying client credentials"); match get_access_token_client(&conf, "nss", "", "") { - Ok(ct) => { - ctc = ct.clone(); - &ctc - }, + Ok(ct) => ct, Err(e) => { error!("Failed to get access token with client credentials: {}", e); return Response::Unavail; @@ -112,20 +92,17 @@ impl PasswdHooks for OidcPasswd { fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> { let conf = nss_hook_prepare(); - let mut cache = get_cache(); - let user = get_current_user(); - let ctc; - let token = match cache.load_user_token(&user) { + if !get_cache().context_user.is_initialized() { + get_cache().context_user.set_current_user(); + } + let token = match get_cache().context_user.get_access_token() { Some(t) => t, None => { // FIXME Implement caching of system token - debug!("Could not find a user token for {} to request NSS data; trying client credentials", user); + debug!("Could not find a user token to request NSS data; trying client credentials"); match get_access_token_client(&conf, "nss", "", "") { - Ok(ct) => { - ctc = ct.clone(); - &ctc - }, + Ok(ct) => ct, Err(e) => { error!("Failed to get access token with client credentials: {}", e); return Response::Unavail; @@ -146,20 +123,16 @@ impl PasswdHooks for OidcPasswd { fn get_entry_by_name(name: String) -> Response<Passwd> { let conf = nss_hook_prepare(); - let mut cache = get_cache(); - - let user = get_current_user(); - let ctc; - let token = match cache.load_user_token(&user) { + if !get_cache().context_user.is_initialized() { + get_cache().context_user.set_current_user(); + } + let token = match get_cache().context_user.get_access_token() { Some(t) => t, None => { // FIXME Implement caching of system token - debug!("Could not find a user token for {} to request NSS data; trying client credentials", user); + debug!("Could not find a user token for to request NSS data; trying client credentials"); match get_access_token_client(&conf, "nss", "", "") { - Ok(ct) => { - ctc = ct.clone(); - &ctc - }, + Ok(ct) => ct, Err(e) => { error!("Failed to get access token with client credentials: {}", e); return Response::Unavail; diff --git a/src/pam.rs b/src/pam.rs index 2133f7f..e98a1fa 100644 --- a/src/pam.rs +++ b/src/pam.rs @@ -90,7 +90,8 @@ impl PamServiceModule for PamOidc { match get_access_token_password(&conf, "pam", username.to_string(), password.to_string(), PamError::SERVICE_ERR, PamError::AUTH_ERR) { Ok(t) => { info!("Authenticated {} using Resource Owner Password Grant", username); - get_cache().save_user_token(&username.to_string(), t.into()); + get_cache().context_user.set_username(username.to_string()); + get_cache().context_user.set_access_token(t); return PamError::SUCCESS; }, Err(e) => { -- GitLab