/* Copyright 2021 Dominik George <dominik.george@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. */ use crate::BASE_NAME; use lazy_static::lazy_static; use std::collections::HashMap; use std::sync::{Mutex, MutexGuard}; use libc::{geteuid, seteuid, getpwnam, uid_t}; use std::ffi::CString; use oauth2::basic::BasicTokenResponse; use std::env; use std::fs; use std::io; use std::path::PathBuf; use xdg::{BaseDirectories, BaseDirectoriesError}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json; const USER_TOKEN_FILENAME: &str = "user_token.json"; pub struct Cache { user_tokens: HashMap<String, BasicTokenResponse>, original_euid: uid_t, } impl Cache { pub fn new() -> Cache { let euid; unsafe { euid = geteuid(); }; Cache { user_tokens: HashMap::new(), original_euid: euid } } fn drop_privileges(&self, username: &String) -> Result<uid_t, &str> { let nam = match CString::new(username.as_str()) { Ok(nam) => nam, Err(_) => return Err("Invalid username in lookup") }; let target_euid; unsafe { target_euid = (*getpwnam(nam.as_ptr())).pw_uid; }; if target_euid == self.original_euid { debug!("No need to drop privileges, already running as {}", username); return Ok(self.original_euid); } else if self.original_euid == 0 { let res; unsafe { res = seteuid(target_euid); }; if res == 0 { debug!("Successfully dropped privileges to {}", username); return Ok(target_euid); } else { error!("Could not drop privileges to {}", username); return Err("Failed to drop privileges"); } } error!("Not running as root or target user, cannot drop privileges"); return Err("Dropping privileges not supported"); } fn restore_privileges(&self) { debug!("Restoring privileges"); unsafe { seteuid(self.original_euid); } } fn get_user_xdg_base_directories(&self, username: &String) -> Result<BaseDirectories, BaseDirectoriesError> { let saved_home = env::var_os("HOME"); let nam = match CString::new(username.as_str()) { Ok(nam) => nam, Err(_) => CString::new("nobody").ok().unwrap() }; let user_home; unsafe { user_home = CString::from_raw((*getpwnam(nam.as_ptr())).pw_dir); }; env::set_var("HOME", user_home.to_str().unwrap()); debug!("Home directory for {} is {}", username, user_home.to_str().unwrap()); let base_dirs = BaseDirectories::with_prefix(BASE_NAME)?; if saved_home != None { env::set_var("HOME", saved_home.unwrap()); } else { env::remove_var("HOME"); } match self.drop_privileges(username) { Ok(_) => match base_dirs.create_cache_directory(BASE_NAME) { Ok(v) => { info!("Created XDG cache directory for user {}", username); Some(v) }, Err(e) => { error!("Error creating XDG cache directory for user {}: {}", username, e); None } } Err(_) => None }; self.restore_privileges(); 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) { Ok(b) => b.place_cache_file(filename), Err(e) => Err(io::Error::new(io::ErrorKind::NotFound, e)) } } pub fn load_user_token(&mut 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()) { Ok(path) => match load_json(path) { Ok(read_token) => Some(read_token), Err(_) => None }, Err(_) => None }; self.restore_privileges(); match new_token { Some(t) => { self.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) } } pub fn save_user_token(&mut self, owner: &String, token: BasicTokenResponse) -> Result<(), io::Error> { self.user_tokens.insert(owner.to_string(), token.clone()); debug!("Saved token for {} in memory", owner); // 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()) { Ok(path) => { debug!("Storing token for {} in cache file", owner); save_json(path, token) }, Err(e) => Err(e) }, Err(e) => Err(io::Error::new(io::ErrorKind::PermissionDenied, e)) }; self.restore_privileges(); return res; } pub fn delete_user_token(&mut self, owner: &String) { self.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> { 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)) } } 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) } lazy_static! { static ref CACHE: Mutex<Cache> = Mutex::new(Cache::new()); } pub fn get_cache() -> MutexGuard<'static, Cache> { CACHE.lock().unwrap() }