Skip to content
Snippets Groups Projects
Verified Commit a6cf14d7 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

[Cache] Document module and remove some unnecessary NSS resolutions

parent e3d51956
No related branches found
No related tags found
No related merge requests found
...@@ -14,6 +14,9 @@ ...@@ -14,6 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
//! This module encapsulates all data handling, both in-memory and
//! backed by disk storage
use crate::BASE_NAME; use crate::BASE_NAME;
use crate::unix::{Passwd, getpwnam_safe, getpwuid_safe}; use crate::unix::{Passwd, getpwnam_safe, getpwuid_safe};
...@@ -34,16 +37,31 @@ use serde::Serialize; ...@@ -34,16 +37,31 @@ use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json; use serde_json;
// FIXME move to config
const USER_TOKEN_FILENAME: &str = "user_token.json"; 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<'a> { pub struct UserInfo<'a> {
/// Numeric user ID
uid: Option<uid_t>, uid: Option<uid_t>,
/// Username as used for authentication
username: Option<String>, username: Option<String>,
/// Passwd struct (once full getpwnam/getpwuid resolution was possible)
passwd: Option<Passwd<'a>>, passwd: Option<Passwd<'a>>,
/// OAuth access token if freshly retrieved or known from disk backed storage
access_token: Option<BasicTokenResponse> access_token: Option<BasicTokenResponse>
} }
/// In-memory structure to hold global process state
/// 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.
pub struct Cache<'a> { pub struct Cache<'a> {
/// The user currently calling the library
/// For NSS, this will be the process owner; for PAM, this will be the user
/// logging in (after successful authentication)
pub context_user: UserInfo<'a> pub context_user: UserInfo<'a>
} }
...@@ -64,14 +82,26 @@ impl Cache<'_> { ...@@ -64,14 +82,26 @@ impl Cache<'_> {
} }
impl <'a>UserInfo<'a> { impl <'a>UserInfo<'a> {
/// 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) { pub fn set_current_user(&mut self) {
self.set_uid(get_original_euid()); self.set_uid(get_original_euid());
} }
/// Returns `true` if any of the information slots is filled
pub fn is_initialized(&self) -> bool { pub fn is_initialized(&self) -> bool {
self.uid.is_some() || self.username.is_some() 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> { fn try_resolve(&mut self) -> Result<(), io::Error> {
// If we already have a full passwd struct, do nothing // If we already have a full passwd struct, do nothing
if self.passwd.is_some() { if self.passwd.is_some() {
...@@ -108,8 +138,12 @@ impl <'a>UserInfo<'a> { ...@@ -108,8 +138,12 @@ impl <'a>UserInfo<'a> {
} }
} }
/// 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> { pub fn get_uid(&mut self) -> Result<uid_t, io::Error> {
self.try_resolve(); if self.uid.is_none() && self.passwd.is_none() {
self.try_resolve();
}
match &self.passwd { match &self.passwd {
Some(passwd) => Ok(passwd.pw_uid), Some(passwd) => Ok(passwd.pw_uid),
None => match self.uid { None => match self.uid {
...@@ -119,15 +153,28 @@ impl <'a>UserInfo<'a> { ...@@ -119,15 +153,28 @@ impl <'a>UserInfo<'a> {
} }
} }
/// Set the numeric user ID, clearing all mismatching fields and attepmting
/// resolution if necessary
pub fn set_uid(&mut self, uid: uid_t) { pub fn set_uid(&mut self, uid: uid_t) {
self.uid = Some(uid); self.uid = Some(uid);
self.username = None;
self.passwd = None; if self.passwd.is_some() && self.passwd.as_ref().unwrap().pw_uid != uid {
self.try_resolve(); // 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> { pub fn get_username(&mut self) -> Result<String, io::Error> {
self.try_resolve(); if self.username.is_none() && self.passwd.is_none() {
self.try_resolve();
}
match &self.passwd { match &self.passwd {
Some(passwd) => Ok(passwd.pw_name.to_string()), Some(passwd) => Ok(passwd.pw_name.to_string()),
None => match &self.username { None => match &self.username {
...@@ -137,21 +184,35 @@ impl <'a>UserInfo<'a> { ...@@ -137,21 +184,35 @@ impl <'a>UserInfo<'a> {
} }
} }
/// Set the username, clearing all mismatching fields and attepmting
/// resolution if necessary
pub fn set_username(&mut self, username: String) { pub fn set_username(&mut self, username: String) {
self.username = Some(username); self.username = Some(username);
self.uid = None;
self.passwd = None; if self.passwd.is_some() && self.passwd.as_ref().unwrap().pw_name != self.username.as_ref().unwrap() {
self.try_resolve(); // 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<&str, io::Error> { pub fn get_home_directory(&mut self) -> Result<&str, io::Error> {
self.try_resolve(); if self.passwd.is_none() {
self.try_resolve();
}
match &self.passwd { match &self.passwd {
Some(passwd) => Ok(passwd.pw_dir), Some(passwd) => Ok(passwd.pw_dir),
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "foo")) 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> { fn drop_privileges(&mut self) -> Result<uid_t, io::Error> {
let current_euid = unsafe { let current_euid = unsafe {
geteuid() geteuid()
...@@ -183,6 +244,8 @@ impl <'a>UserInfo<'a> { ...@@ -183,6 +244,8 @@ impl <'a>UserInfo<'a> {
} }
} }
/// Restore privileges to the original process owner by setting EUID to their user ID
// FIXME Move to global scope
fn restore_privileges(&self) { fn restore_privileges(&self) {
let current_euid = unsafe { let current_euid = unsafe {
geteuid() geteuid()
...@@ -201,13 +264,18 @@ impl <'a>UserInfo<'a> { ...@@ -201,13 +264,18 @@ impl <'a>UserInfo<'a> {
} }
} }
/// Get the XDG base directories for this user
fn get_user_xdg_base_directories(&mut self) -> Result<BaseDirectories, io::Error> { fn get_user_xdg_base_directories(&mut self) -> Result<BaseDirectories, io::Error> {
// Save original $HOME for later restore // Save original $HOME for later restore
let saved_home = env::var_os("HOME"); let saved_home = env::var_os("HOME");
// Determine user ID to find out whether we should override $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()?; let uid = self.get_uid()?;
if uid != get_original_euid() { 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()?; let user_home = self.get_home_directory()?;
env::set_var("HOME", user_home); env::set_var("HOME", user_home);
debug!("Home directory for UID {} is {}", uid, user_home); debug!("Home directory for UID {} is {}", uid, user_home);
...@@ -231,6 +299,7 @@ impl <'a>UserInfo<'a> { ...@@ -231,6 +299,7 @@ impl <'a>UserInfo<'a> {
return Ok(base_dirs); 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> { fn place_user_cache_file(&mut self, filename: String) -> Result<PathBuf, io::Error> {
match self.get_user_xdg_base_directories() { match self.get_user_xdg_base_directories() {
Ok(b) => b.place_cache_file(filename), Ok(b) => b.place_cache_file(filename),
...@@ -238,11 +307,16 @@ impl <'a>UserInfo<'a> { ...@@ -238,11 +307,16 @@ impl <'a>UserInfo<'a> {
} }
} }
/// 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> { pub fn get_access_token(&mut self) -> &Option<BasicTokenResponse> {
// Try to load our acess token if none is known // Try to load our acess token if none is known
if self.access_token.is_none() { if self.access_token.is_none() {
debug!("No token in memory, trying to load from file"); debug!("No token in memory, trying to load from file");
self.drop_privileges().ok(); 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()) { self.access_token = match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) {
Ok(path) => match load_json(path) { Ok(path) => match load_json(path) {
Ok(read_token) => Some(read_token), Ok(read_token) => Some(read_token),
...@@ -256,11 +330,17 @@ impl <'a>UserInfo<'a> { ...@@ -256,11 +330,17 @@ impl <'a>UserInfo<'a> {
return &self.access_token; return &self.access_token;
} }
/// 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> { pub fn set_access_token(&mut self, token: BasicTokenResponse) -> Result<(), io::Error> {
self.access_token = Some(token.clone()); self.access_token = Some(token.clone());
debug!("Saved token for in memory"); debug!("Saved token for in memory");
// Try to write user's token cache file // 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() { let res = match self.drop_privileges() {
Ok(_) => match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) { Ok(_) => match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) {
Ok(path) => { Ok(path) => {
...@@ -276,6 +356,7 @@ impl <'a>UserInfo<'a> { ...@@ -276,6 +356,7 @@ impl <'a>UserInfo<'a> {
} }
} }
/// Deserialize JSON stored in a file on disk
fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> { fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> {
let file = fs::File::open(path)?; let file = fs::File::open(path)?;
let reader = io::BufReader::new(file); let reader = io::BufReader::new(file);
...@@ -285,6 +366,7 @@ fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> { ...@@ -285,6 +366,7 @@ fn load_json<O: DeserializeOwned>(path: PathBuf) -> Result<O, io::Error> {
} }
} }
/// Serialize JSON to a file on disk
fn save_json<O: Serialize>(path: PathBuf, obj: O) -> Result<(), io::Error> { fn save_json<O: Serialize>(path: PathBuf, obj: O) -> Result<(), io::Error> {
let json = match serde_json::to_string(&obj) { let json = match serde_json::to_string(&obj) {
Ok(j) => j, Ok(j) => j,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment