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