openssl = "0.10"
nix = "0.19.1"
regex = "1.2"
-
+once_cell = "1.3.1"
proxmox = { version = "0.13.0", default-features = false, features = [ "cli" ] }
--- /dev/null
+//! Cached user info for fast ACL permission checks
+
+use std::sync::{RwLock, Arc};
+
+use anyhow::{Error, bail};
+
+use proxmox::api::section_config::SectionConfigData;
+use lazy_static::lazy_static;
+use proxmox::api::UserInformation;
+use proxmox::tools::time::epoch_i64;
+
+use pbs_api_types::{Authid, Userid, User, ApiToken, ROLE_ADMIN};
+
+use crate::acl::{AclTree, ROLE_NAMES};
+use crate::memcom::Memcom;
+
+/// Cache User/Group/Token/Acl configuration data for fast permission tests
+pub struct CachedUserInfo {
+ user_cfg: Arc<SectionConfigData>,
+ acl_tree: Arc<AclTree>,
+}
+
+struct ConfigCache {
+ data: Option<Arc<CachedUserInfo>>,
+ last_update: i64,
+ last_user_cache_generation: usize,
+}
+
+lazy_static! {
+ static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
+ ConfigCache { data: None, last_update: 0, last_user_cache_generation: 0 }
+ );
+}
+
+impl CachedUserInfo {
+
+ /// Returns a cached instance (up to 5 seconds old).
+ pub fn new() -> Result<Arc<Self>, Error> {
+ let now = epoch_i64();
+
+ let memcom = Memcom::new()?;
+ let user_cache_generation = memcom.user_cache_generation();
+
+ { // limit scope
+ let cache = CACHED_CONFIG.read().unwrap();
+ if (user_cache_generation == cache.last_user_cache_generation) &&
+ ((now - cache.last_update) < 5)
+ {
+ if let Some(ref config) = cache.data {
+ return Ok(config.clone());
+ }
+ }
+ }
+
+ let config = Arc::new(CachedUserInfo {
+ user_cfg: crate::user::cached_config()?,
+ acl_tree: crate::acl::cached_config()?,
+ });
+
+ let mut cache = CACHED_CONFIG.write().unwrap();
+ cache.last_update = now;
+ cache.last_user_cache_generation = user_cache_generation;
+ cache.data = Some(config.clone());
+
+ Ok(config)
+ }
+
+ /// Only exposed for testing
+ #[doc(hidden)]
+ pub fn test_new(user_cfg: SectionConfigData, acl_tree: AclTree) -> Self {
+ Self {
+ user_cfg: Arc::new(user_cfg),
+ acl_tree: Arc::new(acl_tree),
+ }
+ }
+
+ /// Test if a user_id is enabled and not expired
+ pub fn is_active_user_id(&self, userid: &Userid) -> bool {
+ if let Ok(info) = self.user_cfg.lookup::<User>("user", userid.as_str()) {
+ info.is_active()
+ } else {
+ false
+ }
+ }
+
+ /// Test if a authentication id is enabled and not expired
+ pub fn is_active_auth_id(&self, auth_id: &Authid) -> bool {
+ let userid = auth_id.user();
+
+ if !self.is_active_user_id(userid) {
+ return false;
+ }
+
+ if auth_id.is_token() {
+ if let Ok(info) = self.user_cfg.lookup::<ApiToken>("token", &auth_id.to_string()) {
+ return info.is_active();
+ } else {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ pub fn check_privs(
+ &self,
+ auth_id: &Authid,
+ path: &[&str],
+ required_privs: u64,
+ partial: bool,
+ ) -> Result<(), Error> {
+ let privs = self.lookup_privs(&auth_id, path);
+ let allowed = if partial {
+ (privs & required_privs) != 0
+ } else {
+ (privs & required_privs) == required_privs
+ };
+ if !allowed {
+ // printing the path doesn't leaks any information as long as we
+ // always check privilege before resource existence
+ bail!("no permissions on '/{}'", path.join("/"));
+ }
+ Ok(())
+ }
+
+ pub fn is_superuser(&self, auth_id: &Authid) -> bool {
+ !auth_id.is_token() && auth_id.user() == "root@pam"
+ }
+
+ pub fn is_group_member(&self, _userid: &Userid, _group: &str) -> bool {
+ false
+ }
+
+ pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 {
+ let (privs, _) = self.lookup_privs_details(auth_id, path);
+ privs
+ }
+
+ pub fn lookup_privs_details(&self, auth_id: &Authid, path: &[&str]) -> (u64, u64) {
+ if self.is_superuser(auth_id) {
+ return (ROLE_ADMIN, ROLE_ADMIN);
+ }
+
+ let roles = self.acl_tree.roles(auth_id, path);
+ let mut privs: u64 = 0;
+ let mut propagated_privs: u64 = 0;
+ for (role, propagate) in roles {
+ if let Some((role_privs, _)) = ROLE_NAMES.get(role.as_str()) {
+ if propagate {
+ propagated_privs |= role_privs;
+ }
+ privs |= role_privs;
+ }
+ }
+
+ if auth_id.is_token() {
+ // limit privs to that of owning user
+ let user_auth_id = Authid::from(auth_id.user().clone());
+ privs &= self.lookup_privs(&user_auth_id, path);
+ let (owner_privs, owner_propagated_privs) = self.lookup_privs_details(&user_auth_id, path);
+ privs &= owner_privs;
+ propagated_privs &= owner_propagated_privs;
+ }
+
+ (privs, propagated_privs)
+ }
+
+}
+
+impl UserInformation for CachedUserInfo {
+ fn is_superuser(&self, userid: &str) -> bool {
+ userid == "root@pam"
+ }
+
+ fn is_group_member(&self, _userid: &str, _group: &str) -> bool {
+ false
+ }
+
+ fn lookup_privs(&self, auth_id: &str, path: &[&str]) -> u64 {
+ match auth_id.parse::<Authid>() {
+ Ok(auth_id) => Self::lookup_privs(self, &auth_id, path),
+ Err(_) => 0,
+ }
+ }
+}
pub mod acl;
+mod cached_user_info;
+pub use cached_user_info::CachedUserInfo;
pub mod domains;
pub mod drive;
pub mod key_config;
pub mod tape_encryption_keys;
pub mod tape_job;
pub mod token_shadow;
+pub mod user;
pub mod verify;
+pub(crate) mod memcom;
+
use anyhow::{format_err, Error};
pub use pbs_buildcfg::{BACKUP_USER_NAME, BACKUP_GROUP_NAME};
--- /dev/null
+//! Memory based communication channel between proxy & daemon for things such as cache
+//! invalidation.
+
+use std::os::unix::io::AsRawFd;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Arc;
+
+use anyhow::Error;
+use nix::fcntl::OFlag;
+use nix::sys::mman::{MapFlags, ProtFlags};
+use nix::sys::stat::Mode;
+use once_cell::sync::OnceCell;
+
+use proxmox::tools::fs::CreateOptions;
+use proxmox::tools::mmap::Mmap;
+
+/// In-memory communication channel.
+pub struct Memcom {
+ mmap: Mmap<u8>,
+}
+
+#[repr(C)]
+struct Head {
+ // User (user.cfg) cache generation/version.
+ user_cache_generation: AtomicUsize,
+}
+
+static INSTANCE: OnceCell<Arc<Memcom>> = OnceCell::new();
+
+const MEMCOM_FILE_PATH: &str = pbs_buildcfg::rundir!("/proxmox-backup-memcom");
+const EMPTY_PAGE: [u8; 4096] = [0u8; 4096];
+
+impl Memcom {
+ /// Open the memory based communication channel singleton.
+ pub fn new() -> Result<Arc<Self>, Error> {
+ INSTANCE.get_or_try_init(Self::open).map(Arc::clone)
+ }
+
+ // Actual work of `new`:
+ fn open() -> Result<Arc<Self>, Error> {
+ let user = crate::backup_user()?;
+ let options = CreateOptions::new()
+ .perm(Mode::from_bits_truncate(0o660))
+ .owner(user.uid)
+ .group(user.gid);
+
+ let file = proxmox::tools::fs::atomic_open_or_create_file(
+ MEMCOM_FILE_PATH,
+ OFlag::O_RDWR | OFlag::O_CLOEXEC,
+ &EMPTY_PAGE, options)?;
+
+ let mmap = unsafe {
+ Mmap::<u8>::map_fd(
+ file.as_raw_fd(),
+ 0,
+ 4096,
+ ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
+ MapFlags::MAP_SHARED | MapFlags::MAP_NORESERVE | MapFlags::MAP_POPULATE,
+ )?
+ };
+
+ Ok(Arc::new(Self { mmap }))
+ }
+
+ // Shortcut to get the mapped `Head` as a `Head`.
+ fn head(&self) -> &Head {
+ unsafe { &*(self.mmap.as_ptr() as *const u8 as *const Head) }
+ }
+
+ /// Returns the user cache generation number.
+ pub fn user_cache_generation(&self) -> usize {
+ self.head().user_cache_generation.load(Ordering::Acquire)
+ }
+
+ /// Increase the user cache generation number.
+ pub fn increase_user_cache_generation(&self) {
+ self.head()
+ .user_cache_generation
+ .fetch_add(1, Ordering::AcqRel);
+ }
+}
--- /dev/null
+use std::collections::HashMap;
+use std::sync::{Arc, RwLock};
+
+use anyhow::{bail, Error};
+use lazy_static::lazy_static;
+
+use proxmox::api::{
+ schema::*,
+ section_config::{
+ SectionConfig,
+ SectionConfigData,
+ SectionConfigPlugin,
+ }
+};
+
+use pbs_api_types::{
+ Authid, Userid, ApiToken, User,
+};
+
+use crate::memcom::Memcom;
+
+use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
+
+lazy_static! {
+ pub static ref CONFIG: SectionConfig = init();
+}
+
+fn init() -> SectionConfig {
+ let mut config = SectionConfig::new(&Authid::API_SCHEMA);
+
+ let user_schema = match User::API_SCHEMA {
+ Schema::Object(ref user_schema) => user_schema,
+ _ => unreachable!(),
+ };
+ let user_plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), user_schema);
+ config.register_plugin(user_plugin);
+
+ let token_schema = match ApiToken::API_SCHEMA {
+ Schema::Object(ref token_schema) => token_schema,
+ _ => unreachable!(),
+ };
+ let token_plugin = SectionConfigPlugin::new("token".to_string(), Some("tokenid".to_string()), token_schema);
+ config.register_plugin(token_plugin);
+
+ config
+}
+
+pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
+pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck";
+
+/// Get exclusive lock
+pub fn lock_config() -> Result<BackupLockGuard, Error> {
+ open_backup_lockfile(USER_CFG_LOCKFILE, None, true)
+}
+
+pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
+
+ let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?
+ .unwrap_or_else(|| "".to_string());
+
+ let digest = openssl::sha::sha256(content.as_bytes());
+ let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?;
+
+ if data.sections.get("root@pam").is_none() {
+ let user: User = User {
+ userid: Userid::root_userid().clone(),
+ comment: Some("Superuser".to_string()),
+ enable: None,
+ expire: None,
+ firstname: None,
+ lastname: None,
+ email: None,
+ };
+ data.set_data("root@pam", "user", &user).unwrap();
+ }
+
+ Ok((data, digest))
+}
+
+pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> {
+
+ struct ConfigCache {
+ data: Option<Arc<SectionConfigData>>,
+ last_mtime: i64,
+ last_mtime_nsec: i64,
+ }
+
+ lazy_static! {
+ static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
+ ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 });
+ }
+
+ let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) {
+ Ok(stat) => Some(stat),
+ Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
+ Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err),
+ };
+
+ { // limit scope
+ let cache = CACHED_CONFIG.read().unwrap();
+ if let Some(ref config) = cache.data {
+ if let Some(stat) = stat {
+ if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
+ return Ok(config.clone());
+ }
+ } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
+ return Ok(config.clone());
+ }
+ }
+ }
+
+ let (config, _digest) = config()?;
+ let config = Arc::new(config);
+
+ let mut cache = CACHED_CONFIG.write().unwrap();
+ if let Some(stat) = stat {
+ cache.last_mtime = stat.st_mtime;
+ cache.last_mtime_nsec = stat.st_mtime_nsec;
+ }
+ cache.data = Some(config.clone());
+
+ Ok(config)
+}
+
+pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
+ let raw = CONFIG.write(USER_CFG_FILENAME, &config)?;
+ replace_backup_config(USER_CFG_FILENAME, raw.as_bytes())?;
+
+ // increase user cache generation
+ // We use this in CachedUserInfo
+ let memcom = Memcom::new()?;
+ memcom.increase_user_cache_generation();
+
+ Ok(())
+}
+
+/// Only exposed for testing
+#[doc(hidden)]
+pub fn test_cfg_from_str(raw: &str) -> Result<(SectionConfigData, [u8;32]), Error> {
+ let cfg = init();
+ let parsed = cfg.parse("test_user_cfg", raw)?;
+
+ Ok((parsed, [0;32]))
+}
+
+// shell completion helper
+pub fn complete_userid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
+ match config() {
+ Ok((data, _digest)) => {
+ data.sections.iter()
+ .filter_map(|(id, (section_type, _))| {
+ if section_type == "user" {
+ Some(id.to_string())
+ } else {
+ None
+ }
+ }).collect()
+ },
+ Err(_) => return vec![],
+ }
+}
+
+// shell completion helper
+pub fn complete_authid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
+ match config() {
+ Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(),
+ Err(_) => vec![],
+ }
+}
+
+// shell completion helper
+pub fn complete_token_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
+ let data = match config() {
+ Ok((data, _digest)) => data,
+ Err(_) => return Vec::new(),
+ };
+
+ match param.get("userid") {
+ Some(userid) => {
+ let user = data.lookup::<User>("user", userid);
+ let tokens = data.convert_to_typed_array("token");
+ match (user, tokens) {
+ (Ok(_), Ok(tokens)) => {
+ tokens
+ .into_iter()
+ .filter_map(|token: ApiToken| {
+ let tokenid = token.tokenid;
+ if tokenid.is_token() && tokenid.user() == userid {
+ Some(tokenid.tokenname().unwrap().as_str().to_string())
+ } else {
+ None
+ }
+ }).collect()
+ },
+ _ => vec![],
+ }
+ },
+ None => vec![],
+ }
+}
use pbs_config::acl::AclTreeNode;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
fn extract_acl_node_data(
node: &AclTreeNode,
bail!("parameter 'group' - groups are currently not supported.");
} else if let Some(ref auth_id) = auth_id {
if !delete { // Note: we allow to delete non-existent users
- let user_cfg = crate::config::user::cached_config()?;
+ let user_cfg = pbs_config::user::cached_config()?;
if user_cfg.sections.get(&auth_id.to_string()).is_none() {
bail!(format!("no such {}.",
if auth_id.is_token() { "API token" } else { "user" }));
use crate::auth_helpers::*;
use crate::server::ticket::ApiTicket;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::config::tfa::TfaChallenge;
pub mod acl;
use pbs_config::domains::{OpenIdUserAttribute, OpenIdRealmConfig};
use crate::server::ticket::ApiTicket;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use pbs_config::open_backup_lockfile;
if !user_info.is_active_user_id(&user_id) {
if config.autocreate.unwrap_or(false) {
- use crate::config::user;
+ use pbs_config::user;
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
let user = User {
userid: user_id.clone(),
use pbs_api_types::{Authid, Userid, User, PASSWORD_SCHEMA, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::config::tfa::{TfaInfo, TfaUserData};
/// Perform first-factor (password) authentication only. Ignore password for the root user.
// After authentication, verify that the to-be-modified user actually exists:
if must_exist && authid.user() != userid {
- let (config, _digest) = crate::config::user::config()?;
+ let (config, _digest) = pbs_config::user::config()?;
if config
.lookup::<User>("user", userid.as_str())
};
use pbs_config::token_shadow;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
fn new_user_with_tokens(user: User) -> UserWithTokens {
UserWithTokens {
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<UserWithTokens>, Error> {
- let (config, digest) = crate::config::user::config()?;
+ let (config, digest) = pbs_config::user::config()?;
let auth_id: Authid = rpcenv
.get_auth_id()
rpcenv: &mut dyn RpcEnvironment
) -> Result<(), Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
- let (mut section_config, _digest) = crate::config::user::config()?;
+ let (mut section_config, _digest) = pbs_config::user::config()?;
if section_config.sections.get(config.userid.as_str()).is_some() {
bail!("user '{}' already exists.", config.userid);
// Fails if realm does not exist!
let authenticator = crate::auth::lookup_authenticator(realm)?;
- crate::config::user::save_config(§ion_config)?;
+ pbs_config::user::save_config(§ion_config)?;
if let Some(password) = password {
let user_info = CachedUserInfo::new()?;
)]
/// Read user configuration data.
pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<User, Error> {
- let (config, digest) = crate::config::user::config()?;
+ let (config, digest) = pbs_config::user::config()?;
let user = config.lookup("user", userid.as_str())?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(user)
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
- let (mut config, expected_digest) = crate::config::user::config()?;
+ let (mut config, expected_digest) = pbs_config::user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
config.set_data(userid.as_str(), "user", &data)?;
- crate::config::user::save_config(&config)?;
+ pbs_config::user::save_config(&config)?;
Ok(())
}
/// Remove a user from the configuration file.
pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
let _tfa_lock = crate::config::tfa::write_lock()?;
- let (mut config, expected_digest) = crate::config::user::config()?;
+ let (mut config, expected_digest) = pbs_config::user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
None => bail!("user '{}' does not exist.", userid),
}
- crate::config::user::save_config(&config)?;
+ pbs_config::user::save_config(&config)?;
let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
match authenticator.remove_password(userid.name()) {
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<ApiToken, Error> {
- let (config, digest) = crate::config::user::config()?;
+ let (config, digest) = pbs_config::user::config()?;
let tokenid = Authid::from((userid, Some(tokenname)));
digest: Option<String>,
) -> Result<Value, Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
- let (mut config, expected_digest) = crate::config::user::config()?;
+ let (mut config, expected_digest) = pbs_config::user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
config.set_data(&tokenid_string, "token", &token)?;
- crate::config::user::save_config(&config)?;
+ pbs_config::user::save_config(&config)?;
Ok(json!({
"tokenid": tokenid_string,
digest: Option<String>,
) -> Result<(), Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
- let (mut config, expected_digest) = crate::config::user::config()?;
+ let (mut config, expected_digest) = pbs_config::user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
config.set_data(&tokenid_string, "token", &data)?;
- crate::config::user::save_config(&config)?;
+ pbs_config::user::save_config(&config)?;
Ok(())
}
digest: Option<String>,
) -> Result<(), Error> {
- let _lock = crate::config::user::lock_config()?;
+ let _lock = pbs_config::user::lock_config()?;
- let (mut config, expected_digest) = crate::config::user::config()?;
+ let (mut config, expected_digest) = pbs_config::user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
token_shadow::delete_secret(&tokenid)?;
- crate::config::user::save_config(&config)?;
+ pbs_config::user::save_config(&config)?;
Ok(())
}
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<ApiToken>, Error> {
- let (config, digest) = crate::config::user::config()?;
+ let (config, digest) = pbs_config::user::config()?;
let list:Vec<ApiToken> = config.convert_to_typed_array("token")?;
DataStore, LocalChunkReader,
};
use crate::config::datastore;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::server::{jobstate::Job, WorkerTask};
use pbs_api_types::{DATASTORE_SCHEMA, JOB_ID_SCHEMA, Authid, SyncJobConfig, SyncJobStatus};
use pbs_config::sync;
+use pbs_config::CachedUserInfo;
use crate::{
api2::{
check_sync_job_read_access,
},
},
- config::{
- cached_user_info::CachedUserInfo,
- },
server::{
jobstate::{
Job,
PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_VERIFY,
};
use pbs_config::verify;
+use pbs_config::CachedUserInfo;
use crate::{
api2::types::{
compute_schedule_status,
},
},
- config::cached_user_info::CachedUserInfo,
};
#[api(
use crate::server::{WorkerTask, H2Service};
use crate::backup::DataStore;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
mod environment;
use environment::*;
PROXMOX_CONFIG_DIGEST_SCHEMA, CHANGER_NAME_SCHEMA, SLOT_ARRAY_SCHEMA,
PRIV_TAPE_AUDIT, PRIV_TAPE_MODIFY,
};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
tape::{
linux_tape_changer_list,
check_drive_path,
sync::list_sync_jobs,
verify::list_verification_jobs,
};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::config::datastore::{self, DataStoreConfig, DataStoreConfigUpdater};
use crate::server::{jobstate, WorkerTask};
Authid, LtoTapeDrive, LtoTapeDriveUpdater, ScsiTapeChanger,
PROXMOX_CONFIG_DIGEST_SCHEMA, DRIVE_NAME_SCHEMA, PRIV_TAPE_AUDIT, PRIV_TAPE_MODIFY,
};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
tape::{
lto_tape_device_list,
check_drive_path,
PRIV_TAPE_AUDIT, PRIV_TAPE_MODIFY,
};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
protected: true,
};
use pbs_config::sync;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
input: {
};
use pbs_config::sync;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
pub fn check_sync_job_read_access(
user_info: &CachedUserInfo,
#[test]
fn sync_job_access_test() -> Result<(), Error> {
- let (user_cfg, _) = crate::config::user::test_cfg_from_str(r###"
+ let (user_cfg, _) = pbs_config::user::test_cfg_from_str(r###"
user: noperm@pbs
user: read@pbs
PRIV_TAPE_AUDIT, PRIV_TAPE_MODIFY,
};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
input: {
};
use pbs_config::verify;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
input: {
use crate::tools;
use crate::tools::subscription::{self, SubscriptionStatus, SubscriptionInfo};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
input: {
use crate::api2::types::TaskStateType;
use crate::api2::pull::check_pull_privs;
use crate::server::{self, UPID, UPIDExt, TaskState, TaskListInfoIterator};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
// matches respective job execution privileges
fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) -> Result<(), Error> {
use crate::server::{WorkerTask, jobstate::Job, pull::pull_store};
use crate::backup::DataStore;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
pub fn check_pull_privs(
auth_id: &Authid,
use pbs_datastore::backup_info::BackupDir;
use pbs_datastore::index::IndexFile;
use pbs_datastore::manifest::{archive_type, ArchiveType};
+use pbs_config::CachedUserInfo;
use crate::{
api2::helpers,
WorkerTask,
H2Service,
},
- config::cached_user_info::CachedUserInfo,
};
mod environment;
use crate::backup::DataStore;
use crate::config::datastore;
use crate::tools::statistics::{linear_regression};
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
#[api(
returns: {
use pbs_datastore::{task_log, task_warn, StoreProgress};
use pbs_datastore::backup_info::{BackupDir, BackupInfo};
use pbs_datastore::task::TaskState;
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
server::{
lookup_user_email,
TapeBackupJobSummary,
Authid, ChangerListEntry, LtoTapeDrive, MtxEntryKind, MtxStatusEntry, ScsiTapeChanger,
CHANGER_NAME_SCHEMA, PRIV_TAPE_AUDIT, PRIV_TAPE_READ,
};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
tape::{
TAPE_STATUS_DIR,
Inventory,
use pbs_datastore::task_log;
use pbs_api_types::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
api2::tape::restore::{
fast_catalog_restore,
restore_media,
MediaStatus, MediaContentEntry, MediaContentListFilter,
PRIV_TAPE_AUDIT,
};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
tape::{
TAPE_STATUS_DIR,
Inventory,
use pbs_datastore::index::IndexFile;
use pbs_datastore::manifest::{archive_type, ArchiveType, BackupManifest, MANIFEST_BLOB_NAME};
use pbs_datastore::task::TaskState;
+use pbs_config::CachedUserInfo;
use crate::{
tools::ParallelHandler,
- config::cached_user_info::CachedUserInfo,
backup::DataStore,
server::{
lookup_user_email,
"datastore.cfg" => dump_section_config(&config::datastore::CONFIG),
"tape.cfg" => dump_section_config(&pbs_config::drive::CONFIG),
"tape-job.cfg" => dump_section_config(&pbs_config::tape_job::CONFIG),
- "user.cfg" => dump_section_config(&config::user::CONFIG),
+ "user.cfg" => dump_section_config(&pbs_config::user::CONFIG),
"remote.cfg" => dump_section_config(&pbs_config::remote::CONFIG),
"sync.cfg" => dump_section_config(&pbs_config::sync::CONFIG),
"verification.cfg" => dump_section_config(&pbs_config::verify::CONFIG),
"update",
CliCommand::new(&api2::access::acl::API_METHOD_UPDATE_ACL)
.arg_param(&["path", "role"])
- .completion_cb("auth-id", config::user::complete_authid)
+ .completion_cb("auth-id", pbs_config::user::complete_authid)
.completion_cb("path", config::datastore::complete_acl_path)
);
"update",
CliCommand::new(&api2::access::user::API_METHOD_UPDATE_USER)
.arg_param(&["userid"])
- .completion_cb("userid", config::user::complete_userid)
+ .completion_cb("userid", pbs_config::user::complete_userid)
)
.insert(
"remove",
CliCommand::new(&api2::access::user::API_METHOD_DELETE_USER)
.arg_param(&["userid"])
- .completion_cb("userid", config::user::complete_userid)
+ .completion_cb("userid", pbs_config::user::complete_userid)
)
.insert(
"list-tokens",
CliCommand::new(&&API_METHOD_LIST_TOKENS)
.arg_param(&["userid"])
- .completion_cb("userid", config::user::complete_userid)
+ .completion_cb("userid", pbs_config::user::complete_userid)
)
.insert(
"generate-token",
CliCommand::new(&api2::access::user::API_METHOD_GENERATE_TOKEN)
.arg_param(&["userid", "tokenname"])
- .completion_cb("userid", config::user::complete_userid)
+ .completion_cb("userid", pbs_config::user::complete_userid)
)
.insert(
"delete-token",
CliCommand::new(&api2::access::user::API_METHOD_DELETE_TOKEN)
.arg_param(&["userid", "tokenname"])
- .completion_cb("userid", config::user::complete_userid)
- .completion_cb("tokenname", config::user::complete_token_name)
+ .completion_cb("userid", pbs_config::user::complete_userid)
+ .completion_cb("tokenname", pbs_config::user::complete_token_name)
)
.insert(
"permissions",
CliCommand::new(&&API_METHOD_LIST_PERMISSIONS)
.arg_param(&["auth-id"])
- .completion_cb("auth-id", config::user::complete_authid)
+ .completion_cb("auth-id", pbs_config::user::complete_authid)
.completion_cb("path", config::datastore::complete_acl_path)
);
use pbs_api_types::Authid;
-use proxmox_backup::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use proxmox_backup::server::auth::{ApiAuth, AuthError};
const TICKET_FILE: &str = "/ticket";
+++ /dev/null
-//! Cached user info for fast ACL permission checks
-
-use std::sync::{RwLock, Arc};
-
-use anyhow::{Error, bail};
-
-use proxmox::api::section_config::SectionConfigData;
-use lazy_static::lazy_static;
-use proxmox::api::UserInformation;
-use proxmox::tools::time::epoch_i64;
-
-use pbs_api_types::{Authid, Userid, User, ApiToken, ROLE_ADMIN};
-use pbs_config::acl::{AclTree, ROLE_NAMES};
-
-use crate::tools::Memcom;
-
-/// Cache User/Group/Token/Acl configuration data for fast permission tests
-pub struct CachedUserInfo {
- user_cfg: Arc<SectionConfigData>,
- acl_tree: Arc<AclTree>,
-}
-
-struct ConfigCache {
- data: Option<Arc<CachedUserInfo>>,
- last_update: i64,
- last_user_cache_generation: usize,
-}
-
-lazy_static! {
- static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
- ConfigCache { data: None, last_update: 0, last_user_cache_generation: 0 }
- );
-}
-
-impl CachedUserInfo {
-
- /// Returns a cached instance (up to 5 seconds old).
- pub fn new() -> Result<Arc<Self>, Error> {
- let now = epoch_i64();
-
- let memcom = Memcom::new()?;
- let user_cache_generation = memcom.user_cache_generation();
-
- { // limit scope
- let cache = CACHED_CONFIG.read().unwrap();
- if (user_cache_generation == cache.last_user_cache_generation) &&
- ((now - cache.last_update) < 5)
- {
- if let Some(ref config) = cache.data {
- return Ok(config.clone());
- }
- }
- }
-
- let config = Arc::new(CachedUserInfo {
- user_cfg: super::user::cached_config()?,
- acl_tree: pbs_config::acl::cached_config()?,
- });
-
- let mut cache = CACHED_CONFIG.write().unwrap();
- cache.last_update = now;
- cache.last_user_cache_generation = user_cache_generation;
- cache.data = Some(config.clone());
-
- Ok(config)
- }
-
- #[cfg(test)]
- pub(crate) fn test_new(user_cfg: SectionConfigData, acl_tree: AclTree) -> Self {
- Self {
- user_cfg: Arc::new(user_cfg),
- acl_tree: Arc::new(acl_tree),
- }
- }
-
- /// Test if a user_id is enabled and not expired
- pub fn is_active_user_id(&self, userid: &Userid) -> bool {
- if let Ok(info) = self.user_cfg.lookup::<User>("user", userid.as_str()) {
- info.is_active()
- } else {
- false
- }
- }
-
- /// Test if a authentication id is enabled and not expired
- pub fn is_active_auth_id(&self, auth_id: &Authid) -> bool {
- let userid = auth_id.user();
-
- if !self.is_active_user_id(userid) {
- return false;
- }
-
- if auth_id.is_token() {
- if let Ok(info) = self.user_cfg.lookup::<ApiToken>("token", &auth_id.to_string()) {
- return info.is_active();
- } else {
- return false;
- }
- }
-
- true
- }
-
- pub fn check_privs(
- &self,
- auth_id: &Authid,
- path: &[&str],
- required_privs: u64,
- partial: bool,
- ) -> Result<(), Error> {
- let privs = self.lookup_privs(&auth_id, path);
- let allowed = if partial {
- (privs & required_privs) != 0
- } else {
- (privs & required_privs) == required_privs
- };
- if !allowed {
- // printing the path doesn't leaks any information as long as we
- // always check privilege before resource existence
- bail!("no permissions on '/{}'", path.join("/"));
- }
- Ok(())
- }
-
- pub fn is_superuser(&self, auth_id: &Authid) -> bool {
- !auth_id.is_token() && auth_id.user() == "root@pam"
- }
-
- pub fn is_group_member(&self, _userid: &Userid, _group: &str) -> bool {
- false
- }
-
- pub fn lookup_privs(&self, auth_id: &Authid, path: &[&str]) -> u64 {
- let (privs, _) = self.lookup_privs_details(auth_id, path);
- privs
- }
-
- pub fn lookup_privs_details(&self, auth_id: &Authid, path: &[&str]) -> (u64, u64) {
- if self.is_superuser(auth_id) {
- return (ROLE_ADMIN, ROLE_ADMIN);
- }
-
- let roles = self.acl_tree.roles(auth_id, path);
- let mut privs: u64 = 0;
- let mut propagated_privs: u64 = 0;
- for (role, propagate) in roles {
- if let Some((role_privs, _)) = ROLE_NAMES.get(role.as_str()) {
- if propagate {
- propagated_privs |= role_privs;
- }
- privs |= role_privs;
- }
- }
-
- if auth_id.is_token() {
- // limit privs to that of owning user
- let user_auth_id = Authid::from(auth_id.user().clone());
- privs &= self.lookup_privs(&user_auth_id, path);
- let (owner_privs, owner_propagated_privs) = self.lookup_privs_details(&user_auth_id, path);
- privs &= owner_privs;
- propagated_privs &= owner_propagated_privs;
- }
-
- (privs, propagated_privs)
- }
-
-}
-
-impl UserInformation for CachedUserInfo {
- fn is_superuser(&self, userid: &str) -> bool {
- userid == "root@pam"
- }
-
- fn is_group_member(&self, _userid: &str, _group: &str) -> bool {
- false
- }
-
- fn lookup_privs(&self, auth_id: &str, path: &[&str]) -> u64 {
- match auth_id.parse::<Authid>() {
- Ok(auth_id) => Self::lookup_privs(self, &auth_id, path),
- Err(_) => 0,
- }
- }
-}
use pbs_buildcfg::{self, configdir};
pub mod acme;
-pub mod cached_user_info;
pub mod datastore;
pub mod node;
pub mod tfa;
-pub mod user;
/// Check configuration directory permissions
///
+++ /dev/null
-use std::collections::HashMap;
-use std::sync::{Arc, RwLock};
-
-use anyhow::{bail, Error};
-use lazy_static::lazy_static;
-
-use proxmox::api::{
- schema::*,
- section_config::{
- SectionConfig,
- SectionConfigData,
- SectionConfigPlugin,
- }
-};
-
-use pbs_api_types::{
- Authid, Userid, ApiToken, User,
-};
-use pbs_config::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
-
-use crate::tools::Memcom;
-
-lazy_static! {
- pub static ref CONFIG: SectionConfig = init();
-}
-
-fn init() -> SectionConfig {
- let mut config = SectionConfig::new(&Authid::API_SCHEMA);
-
- let user_schema = match User::API_SCHEMA {
- Schema::Object(ref user_schema) => user_schema,
- _ => unreachable!(),
- };
- let user_plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), user_schema);
- config.register_plugin(user_plugin);
-
- let token_schema = match ApiToken::API_SCHEMA {
- Schema::Object(ref token_schema) => token_schema,
- _ => unreachable!(),
- };
- let token_plugin = SectionConfigPlugin::new("token".to_string(), Some("tokenid".to_string()), token_schema);
- config.register_plugin(token_plugin);
-
- config
-}
-
-pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
-pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck";
-
-/// Get exclusive lock
-pub fn lock_config() -> Result<BackupLockGuard, Error> {
- open_backup_lockfile(USER_CFG_LOCKFILE, None, true)
-}
-
-pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
-
- let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?
- .unwrap_or_else(|| "".to_string());
-
- let digest = openssl::sha::sha256(content.as_bytes());
- let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?;
-
- if data.sections.get("root@pam").is_none() {
- let user: User = User {
- userid: Userid::root_userid().clone(),
- comment: Some("Superuser".to_string()),
- enable: None,
- expire: None,
- firstname: None,
- lastname: None,
- email: None,
- };
- data.set_data("root@pam", "user", &user).unwrap();
- }
-
- Ok((data, digest))
-}
-
-pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> {
-
- struct ConfigCache {
- data: Option<Arc<SectionConfigData>>,
- last_mtime: i64,
- last_mtime_nsec: i64,
- }
-
- lazy_static! {
- static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new(
- ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 });
- }
-
- let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) {
- Ok(stat) => Some(stat),
- Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
- Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err),
- };
-
- { // limit scope
- let cache = CACHED_CONFIG.read().unwrap();
- if let Some(ref config) = cache.data {
- if let Some(stat) = stat {
- if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec {
- return Ok(config.clone());
- }
- } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 {
- return Ok(config.clone());
- }
- }
- }
-
- let (config, _digest) = config()?;
- let config = Arc::new(config);
-
- let mut cache = CACHED_CONFIG.write().unwrap();
- if let Some(stat) = stat {
- cache.last_mtime = stat.st_mtime;
- cache.last_mtime_nsec = stat.st_mtime_nsec;
- }
- cache.data = Some(config.clone());
-
- Ok(config)
-}
-
-pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
- let raw = CONFIG.write(USER_CFG_FILENAME, &config)?;
- replace_backup_config(USER_CFG_FILENAME, raw.as_bytes())?;
-
- // increase user cache generation
- // We use this in CachedUserInfo
- let memcom = Memcom::new()?;
- memcom.increase_user_cache_generation();
-
- Ok(())
-}
-
-#[cfg(test)]
-pub(crate) fn test_cfg_from_str(raw: &str) -> Result<(SectionConfigData, [u8;32]), Error> {
- let cfg = init();
- let parsed = cfg.parse("test_user_cfg", raw)?;
-
- Ok((parsed, [0;32]))
-}
-
-// shell completion helper
-pub fn complete_userid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
- match config() {
- Ok((data, _digest)) => {
- data.sections.iter()
- .filter_map(|(id, (section_type, _))| {
- if section_type == "user" {
- Some(id.to_string())
- } else {
- None
- }
- }).collect()
- },
- Err(_) => return vec![],
- }
-}
-
-// shell completion helper
-pub fn complete_authid(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
- match config() {
- Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(),
- Err(_) => vec![],
- }
-}
-
-// shell completion helper
-pub fn complete_token_name(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
- let data = match config() {
- Ok((data, _digest)) => data,
- Err(_) => return Vec::new(),
- };
-
- match param.get("userid") {
- Some(userid) => {
- let user = data.lookup::<User>("user", userid);
- let tokens = data.convert_to_typed_array("token");
- match (user, tokens) {
- (Ok(_), Ok(tokens)) => {
- tokens
- .into_iter()
- .filter_map(|token: ApiToken| {
- let tokenid = token.tokenid;
- if tokenid.is_token() && tokenid.user() == userid {
- Some(tokenid.tokenname().unwrap().as_str().to_string())
- } else {
- None
- }
- }).collect()
- },
- _ => vec![],
- }
- },
- None => vec![],
- }
-}
use crate::api2::types::{Authid, Userid};
use crate::auth_helpers::*;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::tools;
use hyper::header;
/// Lookup users email address
pub fn lookup_user_email(userid: &Userid) -> Option<String> {
- if let Ok(user_config) = crate::config::user::cached_config() {
+ if let Ok(user_config) = pbs_config::user::cached_config() {
if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
return user.email;
}
use pbs_datastore::backup_info::BackupInfo;
use pbs_datastore::prune::{compute_prune_info, PruneOptions};
use pbs_api_types::{Authid, PRIV_DATASTORE_MODIFY};
+use pbs_config::CachedUserInfo;
use crate::{
- config::cached_user_info::CachedUserInfo,
backup::DataStore,
server::jobstate::Job,
server::WorkerTask,
use crate::api2::types::{Authid, Userid};
use crate::auth_helpers::*;
-use crate::config::cached_user_info::CachedUserInfo;
+use pbs_config::CachedUserInfo;
use crate::tools;
use crate::tools::compression::CompressionMethod;
use crate::tools::FileLogger;
+++ /dev/null
-//! Memory based communication channel between proxy & daemon for things such as cache
-//! invalidation.
-
-use std::os::unix::io::AsRawFd;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::Arc;
-
-use anyhow::Error;
-use nix::fcntl::OFlag;
-use nix::sys::mman::{MapFlags, ProtFlags};
-use nix::sys::stat::Mode;
-use once_cell::sync::OnceCell;
-
-use proxmox::tools::fs::CreateOptions;
-use proxmox::tools::mmap::Mmap;
-
-/// In-memory communication channel.
-pub struct Memcom {
- mmap: Mmap<u8>,
-}
-
-#[repr(C)]
-struct Head {
- // User (user.cfg) cache generation/version.
- user_cache_generation: AtomicUsize,
-}
-
-static INSTANCE: OnceCell<Arc<Memcom>> = OnceCell::new();
-
-const MEMCOM_FILE_PATH: &str = pbs_buildcfg::rundir!("/proxmox-backup-memcom");
-const EMPTY_PAGE: [u8; 4096] = [0u8; 4096];
-
-impl Memcom {
- /// Open the memory based communication channel singleton.
- pub fn new() -> Result<Arc<Self>, Error> {
- INSTANCE.get_or_try_init(Self::open).map(Arc::clone)
- }
-
- // Actual work of `new`:
- fn open() -> Result<Arc<Self>, Error> {
- let user = pbs_config::backup_user()?;
- let options = CreateOptions::new()
- .perm(Mode::from_bits_truncate(0o660))
- .owner(user.uid)
- .group(user.gid);
-
- let file = proxmox::tools::fs::atomic_open_or_create_file(
- MEMCOM_FILE_PATH,
- OFlag::O_RDWR | OFlag::O_CLOEXEC,
- &EMPTY_PAGE, options)?;
-
- let mmap = unsafe {
- Mmap::<u8>::map_fd(
- file.as_raw_fd(),
- 0,
- 4096,
- ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
- MapFlags::MAP_SHARED | MapFlags::MAP_NORESERVE | MapFlags::MAP_POPULATE,
- )?
- };
-
- Ok(Arc::new(Self { mmap }))
- }
-
- // Shortcut to get the mapped `Head` as a `Head`.
- fn head(&self) -> &Head {
- unsafe { &*(self.mmap.as_ptr() as *const u8 as *const Head) }
- }
-
- /// Returns the user cache generation number.
- pub fn user_cache_generation(&self) -> usize {
- self.head().user_cache_generation.load(Ordering::Acquire)
- }
-
- /// Increase the user cache generation number.
- pub fn increase_user_cache_generation(&self) {
- self.head()
- .user_cache_generation
- .fetch_add(1, Ordering::AcqRel);
- }
-}
pub mod daemon;
pub mod disks;
-mod memcom;
-pub use memcom::Memcom;
-
pub mod serde_filter;
pub mod statistics;
pub mod subscription;