Skip to content
Snippets Groups Projects
cache.rs 14.7 KiB
Newer Older
/* 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 libc::{geteuid, seteuid, uid_t};
use oauth2::basic::BasicTokenResponse;

use std::io;
use std::path::PathBuf;
use xdg::BaseDirectories;
use serde::Serialize;
use serde::de::DeserializeOwned;
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 {
    /// Username as used for authentication
    /// Passwd struct (once full getpwnam/getpwuid resolution was possible)
    passwd: Option<Passwd>,
    /// OAuth access token if freshly retrieved or known from disk backed storage
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
    /// Returns `true` if any of the information slots is filled
        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
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    /// 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");
        }

        // If we cannot call getpwnam safely, return error (see `is_get_pwnam_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);
    /// 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() {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            self.try_resolve().ok();
        match &self.passwd {
            Some(passwd) => Ok(passwd.pw_uid),
            None => match self.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;
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            self.try_resolve().ok();
        }
        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() {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            self.try_resolve().ok();
        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) {
        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;
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            self.try_resolve().ok();
        }
        self.uid = match &self.passwd {
            Some(p) => Some(p.pw_uid),
            None => None
        };
    /// Set the full passwd struct from outside
    pub fn set_passwd(&mut self, passwd: Passwd) {
        self.passwd = Some(passwd.clone());
        self.username = Some(passwd.pw_name);
        self.uid = Some(passwd.pw_uid);
    }

    /// 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> {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
            self.try_resolve().ok();
            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,
                debug!("Could not drop privileges because target UID is not resolved");
        if target_euid == current_euid {
            debug!("No need to drop privileges, already running as {}", current_euid);
            return Ok(current_euid);
            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);
    /// 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
            // Determine home directory and override $HOME to make the XDG code return
            // XDG directories for a different user
            env::set_var("HOME", &user_home);
            debug!("Home directory for UID {} is {}", uid, user_home);
        }
        let base_dirs = match BaseDirectories::with_prefix(BASE_NAME) {
            Ok(b) => b,
            Err(e) => {
                error!("Could not determine XDG base directories: {}", e);
                return Err(io::Error::new(io::ErrorKind::Other, e));
            }

        // Restore $HOME to original if we changed it earlier
            if saved_home != None {
                env::set_var("HOME", saved_home.unwrap());
            } else {
                env::remove_var("HOME");
            }
    /// 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) => {
                error!("Error placing cache file {}: {}", filename, 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
            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, persist: bool) -> Result<(), io::Error> {
        debug!("Saved token in memory");
        if persist {
            // 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) => {
                        error!("Error getting cache path in user home: {}", e);
                        Err(e)
                    }
                    error!("Error dropping privileges to store token in user home: {}", e);
/// 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) {
        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)
}
Nik | Klampfradler's avatar
Nik | Klampfradler committed
static mut IS_GETPWNAM_SAFE: bool = true;
fn get_is_getpwnam_safe() -> bool {
    unsafe {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        IS_GETPWNAM_SAFE
    }
}
pub fn set_is_getpwnam_safe(v: bool) {
    unsafe {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        IS_GETPWNAM_SAFE = v
/// Restore privileges to the original process owner by setting EUID to their user ID
fn restore_privileges() {
    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");
    }
}

Nik | Klampfradler's avatar
Nik | Klampfradler committed
static mut ORIGINAL_EUID: uid_t = uid_t::MAX;
static mut ORIGINAL_EUID_SET: bool = false;
fn get_original_euid() -> uid_t {
    unsafe {
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        if !ORIGINAL_EUID_SET {
            ORIGINAL_EUID = geteuid();
            debug!("Original EUID stored as {}", ORIGINAL_EUID);
            ORIGINAL_EUID_SET = true;
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        ORIGINAL_EUID
    /// 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> {
    // Ensure original EUID is set before first action on context user
    get_original_euid();

    CONTEXT_USER.lock().unwrap()