/* Copyright 2021 Dominik George <dominik.george@teckids.org> * Copyright 2021 mirabilos <thorsten.glaser@teckids.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //! This module encapsulates all data handling, both in-memory and //! backed by disk storage use crate::BASE_NAME; use crate::unix::{Passwd, getpwnam_safe, getpwuid_safe}; use lazy_static::lazy_static; use std::sync::{Mutex, MutexGuard}; use libc::{geteuid, seteuid, uid_t}; use oauth2::basic::BasicTokenResponse; use std::env; use std::fs; use std::io; use std::path::PathBuf; use xdg::BaseDirectories; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json; // FIXME move to config const USER_TOKEN_FILENAME: &str = "user_token.json"; /// Holds (partial or full) information about a user /// This will mostly be the context user (see `Cache`), and is filled with /// as much detail about the user as is available pub struct UserInfo { /// Numeric user ID uid: Option<uid_t>, /// Username as used for authentication username: Option<String>, /// Passwd struct (once full getpwnam/getpwuid resolution was possible) passwd: Option<Passwd>, /// OAuth access token if freshly retrieved or known from disk backed storage access_token: Option<BasicTokenResponse> } impl UserInfo { pub fn new() -> UserInfo { Self { uid: None, username: None, passwd: None, access_token: None } } /// Set the information of this user object to that of the process owner // FIXME Move to Cache, with a from_current_user generator method here pub fn set_current_user(&mut self) { self.set_uid(get_original_euid()); } /// Returns `true` if any of the information slots is filled pub fn is_initialized(&self) -> bool { self.passwd.is_some() || self.uid.is_some() || self.username.is_some() } /// Try to do `getpwnam`/`getpwuid` resolution for this user /// /// Will fill the `passwd` slot on success, or return an error if not successful. /// This method will only attempt resolution if calling `getpwnam`/`getpwuid` is /// currently considered safe, i.e. the `is_getpwnam_safe` flag has not been set /// to `false`. It will be set to false if another resolution is currently running, /// because libc will call back into our backend and we need to break the loop. /// This means that e.g. home directory resolution is impossible during an NSS /// backend call, because we cannot call NSS again. fn try_resolve(&mut self) -> Result<(), io::Error> { // If we already have a full passwd struct, do nothing if self.passwd.is_some() { debug!("passwd entry for context user already resolved"); return Ok(()); } // If we cannot call getpwnam safely, return error (see `is_get_pwnam_safe`) if !get_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(()) }, Err(e) => Err(e) } } /// Return the numeric user ID from either the passwd struct or the uid slot, /// attempting NSS resolution before doing so (in case only username is filled) pub fn get_uid(&mut self) -> Result<uid_t, io::Error> { if self.uid.is_none() && self.passwd.is_none() { self.try_resolve(); } match &self.passwd { Some(passwd) => Ok(passwd.pw_uid), None => match self.uid { Some(uid) => Ok(uid), None => Err(io::Error::new(io::ErrorKind::InvalidInput, "foo")) } } } /// Set the numeric user ID, clearing all mismatching fields and attepmting /// resolution if necessary pub fn set_uid(&mut self, uid: uid_t) { self.uid = Some(uid); if self.passwd.is_some() && self.passwd.as_ref().unwrap().pw_uid != uid { // Invalidate passwd because UID does not match anymore self.passwd = None; self.try_resolve(); } self.username = match &self.passwd { Some(p) => Some(p.pw_name.to_string()), None => None }; } /// Return the username from either the passwd struct or the username slot, /// attempting NSS resolution before doing so (in case only uid is filled) pub fn get_username(&mut self) -> Result<String, io::Error> { if self.username.is_none() && self.passwd.is_none() { self.try_resolve(); } match &self.passwd { Some(passwd) => Ok(passwd.pw_name.to_string()), None => match &self.username { Some(username) => Ok(username.to_string()), None => Err(io::Error::new(io::ErrorKind::InvalidInput, "foo")) } } } /// Set the username, clearing all mismatching fields and attepmting /// resolution if necessary pub fn set_username(&mut self, username: String) { self.username = Some(username); if self.passwd.is_some() && self.passwd.as_ref().unwrap().pw_name != self.username.as_ref().unwrap().to_string() { // Invalidate passwd because UID does not match anymore self.passwd = None; self.try_resolve(); } self.uid = match &self.passwd { Some(p) => Some(p.pw_uid), None => None }; } /// Return the home directory from the passwd slot, /// attempting NSS resolution before doing so pub fn get_home_directory(&mut self) -> Result<String, io::Error> { if self.passwd.is_none() { self.try_resolve(); } match &self.passwd { Some(passwd) => Ok(passwd.pw_dir.clone()), None => Err(io::Error::new(io::ErrorKind::InvalidInput, "foo")) } } /// Attempt to drop privileges to this user, by setting EUID to their user ID fn drop_privileges(&mut self) -> Result<uid_t, io::Error> { let current_euid = unsafe { geteuid() }; let target_euid = match self.get_uid() { Ok(uid) => uid, Err(e) => { debug!("Could not drop privileges because target UID is not resolved"); return Err(e); } }; if target_euid == current_euid { debug!("No need to drop privileges, already running as {}", current_euid); return Ok(current_euid); } let res = unsafe { seteuid(target_euid) }; if res == 0 { 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 {}", target_euid); return Err(e); } } /// Restore privileges to the original process owner by setting EUID to their user ID // FIXME Move to global scope fn restore_privileges(&self) { let current_euid = unsafe { geteuid() }; if current_euid != get_original_euid() { debug!("Restoring privileges"); let res = unsafe { seteuid(get_original_euid()) }; if res != 0 { panic!("Could not restore privileges to {}", get_original_euid()); } } else { debug!("No need to restore privileges, already running as original user"); } } /// Get the XDG base directories for this user 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"); // Determine user ID to find out whether we should override $HOME // For the current user, we rely on $HOME being set to avoid a bootstrapping // issue to get the access token for NSS resolution let uid = self.get_uid()?; if uid != get_original_euid() { // Determine home directory and override $HOME to make the XDG code return // XDG directories for a different user 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)) }; // Restore $HOME to original if we changed it earlier if uid != get_original_euid() { if saved_home != None { env::set_var("HOME", saved_home.unwrap()); } else { env::remove_var("HOME"); } } return Ok(base_dirs); } /// Get the full path to a cache file under our prefix in this user's XDG cache directory 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(e) } } /// Get a known access token for this user /// /// This will use the in-memory token from the `access_token` slot if it is filled, /// or attempt to load a token from disk if not 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(); // Trying to read even after failed privilege dropping is safe 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 }, Err(_) => None }; self.restore_privileges(); } match &self.access_token { Some(t) => Some(t.clone()), None => None } } /// Set the known access token for this user /// /// This will store the token in memory in the `access_token` slot, and attempt to /// write the token to disk afterwards 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 // We need to ensure privileges were dropped successfully to avoid symlink attacks // cf. https://capec.mitre.org/data/definitions/132.html 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"); save_json(path, token) }, Err(e) => Err(e) }, Err(e) => Err(e) }; self.restore_privileges(); return res; } } /// Deserialize JSON stored in a file on disk fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> { let file = fs::File::open(path)?; let reader = io::BufReader::new(file); match serde_json::from_reader(reader) { Ok(o) => Ok(o), Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)) } } /// Serialize JSON to a file on disk fn save_json<O: Serialize>(path: PathBuf, obj: O) -> Result<(), io::Error> { let json = match serde_json::to_string(&obj) { Ok(j) => j, Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)) }; fs::write(path, json) } static mut is_getpwnam_safe: bool = true; fn get_is_getpwnam_safe() -> bool { unsafe { is_getpwnam_safe } } pub fn set_is_getpwnam_safe(v: bool) { unsafe { is_getpwnam_safe = v } } static mut original_euid: uid_t = uid_t::MAX; static mut original_euid_set: bool = false; fn get_original_euid() -> uid_t { unsafe { if !original_euid_set { original_euid = geteuid(); original_euid_set = true; } original_euid } } lazy_static! { /// Current context user /// Needed because the PAM and NSS components might be used chained (if NSS /// resolution is necessary to complete PAM authentication), and we need to /// remember state between the calls. static ref CONTEXT_USER: Mutex<UserInfo> = Mutex::new(UserInfo::new()); } pub fn get_context_user() -> MutexGuard<'static, UserInfo> { CONTEXT_USER.lock().unwrap() }