From e7cb4dc50d1abf3acaeddbc980c20f5285d03e17 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 6 Aug 2020 15:46:01 +0200 Subject: [PATCH] introduce Username, Realm and Userid api types and begin splitting up types.rs as it has grown quite large already Signed-off-by: Wolfgang Bumiller --- examples/download-speed.rs | 3 +- examples/upload-speed.rs | 3 +- src/api2/access.rs | 35 +-- src/api2/access/acl.rs | 6 +- src/api2/access/user.rs | 38 ++- src/api2/admin/datastore.rs | 112 +++++---- src/api2/admin/sync.rs | 16 +- src/api2/backup.rs | 14 +- src/api2/backup/environment.rs | 9 +- src/api2/config/remote.rs | 6 +- src/api2/node.rs | 16 +- src/api2/node/apt.rs | 6 +- src/api2/node/disks.rs | 6 +- src/api2/node/disks/directory.rs | 4 +- src/api2/node/disks/zfs.rs | 4 +- src/api2/node/network.rs | 4 +- src/api2/node/tasks.rs | 34 +-- src/api2/pull.rs | 16 +- src/api2/reader.rs | 13 +- src/api2/reader/environment.rs | 9 +- src/api2/status.rs | 14 +- src/api2/types/macros.rs | 4 + src/api2/types/mod.rs | 81 +++---- src/api2/types/userid.rs | 376 ++++++++++++++++++++++++++++++ src/auth.rs | 67 +++--- src/auth_helpers.rs | 13 +- src/backup/datastore.rs | 20 +- src/bin/proxmox-backup-client.rs | 2 +- src/bin/proxmox-backup-manager.rs | 11 +- src/bin/proxmox-backup-proxy.rs | 11 +- src/client/backup_repo.rs | 13 +- src/client/http_client.rs | 28 ++- src/client/pull.rs | 8 +- src/config/acl.rs | 122 +++++----- src/config/cached_user_info.rs | 38 ++- src/config/remote.rs | 4 +- src/config/user.rs | 8 +- src/server/rest.rs | 27 +-- src/server/upid.rs | 30 ++- src/server/worker_task.rs | 13 +- src/tools/ticket.rs | 20 +- tests/worker-task-abort.rs | 30 ++- 42 files changed, 877 insertions(+), 417 deletions(-) create mode 100644 src/api2/types/macros.rs create mode 100644 src/api2/types/userid.rs diff --git a/examples/download-speed.rs b/examples/download-speed.rs index 09f5a918..694c55d2 100644 --- a/examples/download-speed.rs +++ b/examples/download-speed.rs @@ -4,6 +4,7 @@ use anyhow::{Error}; use chrono::{DateTime, Utc}; +use proxmox_backup::api2::types::Userid; use proxmox_backup::client::{HttpClient, HttpClientOptions, BackupReader}; pub struct DummyWriter { @@ -27,7 +28,7 @@ async fn run() -> Result<(), Error> { let host = "localhost"; - let username = "root@pam"; + let username = Userid::root_userid(); let options = HttpClientOptions::new() .interactive(true) diff --git a/examples/upload-speed.rs b/examples/upload-speed.rs index 8594d6ea..bb6add15 100644 --- a/examples/upload-speed.rs +++ b/examples/upload-speed.rs @@ -1,5 +1,6 @@ use anyhow::{Error}; +use proxmox_backup::api2::types::Userid; use proxmox_backup::client::*; async fn upload_speed() -> Result { @@ -7,7 +8,7 @@ async fn upload_speed() -> Result { let host = "localhost"; let datastore = "store2"; - let username = "root@pam"; + let username = Userid::root_userid(); let options = HttpClientOptions::new() .interactive(true) diff --git a/src/api2/access.rs b/src/api2/access.rs index 7a87a51f..6ad70e6e 100644 --- a/src/api2/access.rs +++ b/src/api2/access.rs @@ -2,7 +2,7 @@ use anyhow::{bail, format_err, Error}; use serde_json::{json, Value}; -use proxmox::api::{api, RpcEnvironment, Permission, UserInformation}; +use proxmox::api::{api, RpcEnvironment, Permission}; use proxmox::api::router::{Router, SubdirMap}; use proxmox::{sortable, identity}; use proxmox::{http_err, list_subdirs_api_method}; @@ -23,7 +23,7 @@ pub mod role; /// returns Ok(true) if a ticket has to be created /// and Ok(false) if not fn authenticate_user( - username: &str, + userid: &Userid, password: &str, path: Option, privs: Option, @@ -31,7 +31,7 @@ fn authenticate_user( ) -> Result { let user_info = CachedUserInfo::new()?; - if !user_info.is_active_user(&username) { + if !user_info.is_active_user(&userid) { bail!("user account disabled or expired."); } @@ -39,10 +39,10 @@ fn authenticate_user( if password.starts_with("PBS:") { if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) { - if ticket_username == username { + if *userid == ticket_username { return Ok(true); } else { - bail!("ticket login failed - wrong username"); + bail!("ticket login failed - wrong userid"); } } } else if password.starts_with("PBSTERM:") { @@ -55,7 +55,7 @@ fn authenticate_user( let port = port.unwrap(); if let Ok((_age, _data)) = - tools::ticket::verify_term_ticket(public_auth_key(), &username, &path, port, password) + tools::ticket::verify_term_ticket(public_auth_key(), &userid, &path, port, password) { for (name, privilege) in PRIVILEGES { if *name == privilege_name { @@ -66,7 +66,7 @@ fn authenticate_user( } } - user_info.check_privs(username, &path_vec, *privilege, false)?; + user_info.check_privs(userid, &path_vec, *privilege, false)?; return Ok(false); } } @@ -75,7 +75,7 @@ fn authenticate_user( } } - let _ = crate::auth::authenticate_user(username, password)?; + let _ = crate::auth::authenticate_user(userid, password)?; Ok(true) } @@ -83,7 +83,7 @@ fn authenticate_user( input: { properties: { username: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, password: { schema: PASSWORD_SCHEMA, @@ -130,7 +130,7 @@ fn authenticate_user( /// /// Returns: An authentication ticket with additional infos. fn create_ticket( - username: String, + username: Userid, password: String, path: Option, privs: Option, @@ -165,7 +165,7 @@ fn create_ticket( input: { properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, password: { schema: PASSWORD_SCHEMA, @@ -183,13 +183,15 @@ fn create_ticket( /// Each user is allowed to change his own password. Superuser /// can change all passwords. fn change_password( - userid: String, + userid: Userid, password: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let current_user = rpcenv.get_user() - .ok_or_else(|| format_err!("unknown user"))?; + let current_user: Userid = rpcenv + .get_user() + .ok_or_else(|| format_err!("unknown user"))? + .parse()?; let mut allowed = userid == current_user; @@ -205,9 +207,8 @@ fn change_password( bail!("you are not authorized to change the password."); } - let (username, realm) = crate::auth::parse_userid(&userid)?; - let authenticator = crate::auth::lookup_authenticator(&realm)?; - authenticator.store_password(&username, &password)?; + let authenticator = crate::auth::lookup_authenticator(userid.realm())?; + authenticator.store_password(userid.name(), &password)?; Ok(Value::Null) } diff --git a/src/api2/access/acl.rs b/src/api2/access/acl.rs index 7187538f..96154226 100644 --- a/src/api2/access/acl.rs +++ b/src/api2/access/acl.rs @@ -142,7 +142,7 @@ pub fn read_acl( }, userid: { optional: true, - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, group: { optional: true, @@ -168,7 +168,7 @@ pub fn update_acl( path: String, role: String, propagate: Option, - userid: Option, + userid: Option, group: Option, delete: Option, digest: Option, @@ -193,7 +193,7 @@ pub fn update_acl( } else if let Some(ref userid) = userid { if !delete { // Note: we allow to delete non-existent users let user_cfg = crate::config::user::cached_config()?; - if user_cfg.sections.get(userid).is_none() { + if user_cfg.sections.get(&userid.to_string()).is_none() { bail!("no such user."); } } diff --git a/src/api2/access/user.rs b/src/api2/access/user.rs index b3f70dcf..ad50f2d9 100644 --- a/src/api2/access/user.rs +++ b/src/api2/access/user.rs @@ -49,7 +49,7 @@ pub fn list_users( input: { properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, comment: { schema: SINGLE_LINE_COMMENT_SCHEMA, @@ -94,19 +94,18 @@ pub fn create_user(password: Option, param: Value) -> Result<(), Error> let (mut config, _digest) = user::config()?; - if let Some(_) = config.sections.get(&user.userid) { + if let Some(_) = config.sections.get(user.userid.as_str()) { bail!("user '{}' already exists.", user.userid); } - let (username, realm) = crate::auth::parse_userid(&user.userid)?; - let authenticator = crate::auth::lookup_authenticator(&realm)?; + let authenticator = crate::auth::lookup_authenticator(&user.userid.realm())?; - config.set_data(&user.userid, "user", &user)?; + config.set_data(user.userid.as_str(), "user", &user)?; user::save_config(&config)?; if let Some(password) = password { - authenticator.store_password(&username, &password)?; + authenticator.store_password(user.userid.name(), &password)?; } Ok(()) @@ -116,7 +115,7 @@ pub fn create_user(password: Option, param: Value) -> Result<(), Error> input: { properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, }, }, @@ -129,9 +128,9 @@ pub fn create_user(password: Option, param: Value) -> Result<(), Error> }, )] /// Read user configuration data. -pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result { +pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result { let (config, digest) = user::config()?; - let user = config.lookup("user", &userid)?; + let user = config.lookup("user", userid.as_str())?; rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); Ok(user) } @@ -141,7 +140,7 @@ pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result< input: { properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, comment: { optional: true, @@ -183,7 +182,7 @@ pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result< )] /// Update user configuration. pub fn update_user( - userid: String, + userid: Userid, comment: Option, enable: Option, expire: Option, @@ -203,7 +202,7 @@ pub fn update_user( crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; } - let mut data: user::User = config.lookup("user", &userid)?; + let mut data: user::User = config.lookup("user", userid.as_str())?; if let Some(comment) = comment { let comment = comment.trim().to_string(); @@ -223,9 +222,8 @@ pub fn update_user( } if let Some(password) = password { - let (username, realm) = crate::auth::parse_userid(&userid)?; - let authenticator = crate::auth::lookup_authenticator(&realm)?; - authenticator.store_password(&username, &password)?; + let authenticator = crate::auth::lookup_authenticator(userid.realm())?; + authenticator.store_password(userid.name(), &password)?; } if let Some(firstname) = firstname { @@ -239,7 +237,7 @@ pub fn update_user( data.email = if email.is_empty() { None } else { Some(email) }; } - config.set_data(&userid, "user", &data)?; + config.set_data(userid.as_str(), "user", &data)?; user::save_config(&config)?; @@ -251,7 +249,7 @@ pub fn update_user( input: { properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, digest: { optional: true, @@ -264,7 +262,7 @@ pub fn update_user( }, )] /// Remove a user from the configuration file. -pub fn delete_user(userid: String, digest: Option) -> Result<(), Error> { +pub fn delete_user(userid: Userid, digest: Option) -> Result<(), Error> { let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?; @@ -275,8 +273,8 @@ pub fn delete_user(userid: String, digest: Option) -> Result<(), Error> crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; } - match config.sections.get(&userid) { - Some(_) => { config.sections.remove(&userid); }, + match config.sections.get(userid.as_str()) { + Some(_) => { config.sections.remove(userid.as_str()); }, None => bail!("user '{}' does not exist.", userid), } diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 97fd9093..a0a14a2f 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -10,7 +10,8 @@ use serde_json::{json, Value}; use proxmox::api::{ api, ApiResponseFuture, ApiHandler, ApiMethod, Router, - RpcEnvironment, RpcEnvironmentType, Permission, UserInformation}; + RpcEnvironment, RpcEnvironmentType, Permission +}; use proxmox::api::router::SubdirMap; use proxmox::api::schema::*; use proxmox::tools::fs::{replace_file, CreateOptions}; @@ -36,7 +37,11 @@ use crate::config::acl::{ PRIV_DATASTORE_BACKUP, }; -fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> Result<(), Error> { +fn check_backup_owner( + store: &DataStore, + group: &BackupGroup, + userid: &Userid, +) -> Result<(), Error> { let owner = store.get_owner(group)?; if &owner != userid { bail!("backup owner check failed ({} != {})", userid, owner); @@ -44,7 +49,10 @@ fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> R Ok(()) } -fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result<(BackupManifest, Vec), Error> { +fn read_backup_index( + store: &DataStore, + backup_dir: &BackupDir, +) -> Result<(BackupManifest, Vec), Error> { let (manifest, index_size) = store.load_manifest(backup_dir)?; @@ -131,9 +139,9 @@ fn list_groups( rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let datastore = DataStore::lookup_datastore(&store)?; @@ -154,7 +162,7 @@ fn list_groups( let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; let owner = datastore.get_owner(group)?; if !list_all { - if owner != username { continue; } + if owner != userid { continue; } } let result_item = GroupListItem { @@ -212,16 +220,16 @@ pub fn list_snapshot_files( rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let datastore = DataStore::lookup_datastore(&store)?; let snapshot = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0; - if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; } let info = BackupInfo::new(&datastore.base_path(), snapshot)?; @@ -264,16 +272,16 @@ fn delete_snapshot( rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let snapshot = BackupDir::new(backup_type, backup_id, backup_time); let datastore = DataStore::lookup_datastore(&store)?; let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; - if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; } datastore.remove_backup_dir(&snapshot, false)?; @@ -320,9 +328,9 @@ pub fn list_snapshots ( rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let datastore = DataStore::lookup_datastore(&store)?; @@ -345,7 +353,7 @@ pub fn list_snapshots ( let owner = datastore.get_owner(group)?; if !list_all { - if owner != username { continue; } + if owner != userid { continue; } } let mut size = None; @@ -481,12 +489,15 @@ pub fn verify( _ => bail!("parameters do not spefify a backup group or snapshot"), } - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; let upid_str = WorkerTask::new_thread( - "verify", Some(worker_id.clone()), &username, to_stdout, move |worker| - { + "verify", + Some(worker_id.clone()), + userid, + to_stdout, + move |worker| { let failed_dirs = if let Some(backup_dir) = backup_dir { let mut verified_chunks = HashSet::with_capacity(1024*16); let mut corrupt_chunks = HashSet::with_capacity(64); @@ -508,7 +519,8 @@ pub fn verify( bail!("verfication failed - please check the log for details"); } Ok(()) - })?; + }, + )?; Ok(json!(upid_str)) } @@ -593,9 +605,9 @@ fn prune( let backup_type = tools::required_string_param(¶m, "backup-type")?; let backup_id = tools::required_string_param(¶m, "backup-id")?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let dry_run = param["dry-run"].as_bool().unwrap_or(false); @@ -604,7 +616,7 @@ fn prune( let datastore = DataStore::lookup_datastore(&store)?; let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; - if !allowed { check_backup_owner(&datastore, &group, &username)?; } + if !allowed { check_backup_owner(&datastore, &group, &userid)?; } let prune_options = PruneOptions { keep_last: param["keep-last"].as_u64(), @@ -646,7 +658,7 @@ fn prune( // We use a WorkerTask just to have a task log, but run synchrounously - let worker = WorkerTask::new("prune", Some(worker_id), "root@pam", true)?; + let worker = WorkerTask::new("prune", Some(worker_id), Userid::root_userid().clone(), true)?; let result = try_block! { if keep_all { @@ -728,11 +740,15 @@ fn start_garbage_collection( let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; let upid_str = WorkerTask::new_thread( - "garbage_collection", Some(store.clone()), "root@pam", to_stdout, move |worker| - { + "garbage_collection", + Some(store.clone()), + Userid::root_userid().clone(), + to_stdout, + move |worker| { worker.log(format!("starting garbage collection on store {}", store)); datastore.garbage_collection(&worker) - })?; + }, + )?; Ok(json!(upid_str)) } @@ -796,13 +812,13 @@ fn get_datastore_list( let (config, _digest) = datastore::config()?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; let mut list = Vec::new(); for (store, (_, data)) in &config.sections { - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0; if allowed { let mut entry = json!({ "store": store }); @@ -847,9 +863,9 @@ fn download_file( let store = tools::required_string_param(¶m, "store")?; let datastore = DataStore::lookup_datastore(store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let file_name = tools::required_string_param(¶m, "file-name")?.to_owned(); @@ -860,7 +876,7 @@ fn download_file( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } println!("Download {} from {} ({}/{})", file_name, store, backup_dir, file_name); @@ -920,9 +936,9 @@ fn download_file_decoded( let store = tools::required_string_param(¶m, "store")?; let datastore = DataStore::lookup_datastore(store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let file_name = tools::required_string_param(¶m, "file-name")?.to_owned(); @@ -933,7 +949,7 @@ fn download_file_decoded( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } let (_manifest, files) = read_backup_index(&datastore, &backup_dir)?; for file in files { @@ -1038,8 +1054,8 @@ fn upload_backup_log( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); - let username = rpcenv.get_user().unwrap(); - check_backup_owner(&datastore, backup_dir.group(), &username)?; + let userid: Userid = rpcenv.get_user().unwrap().parse()?; + check_backup_owner(&datastore, backup_dir.group(), &userid)?; let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); @@ -1108,14 +1124,14 @@ fn catalog( ) -> Result { let datastore = DataStore::lookup_datastore(&store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); @@ -1207,9 +1223,9 @@ fn pxar_file_download( let store = tools::required_string_param(¶m, "store")?; let datastore = DataStore::lookup_datastore(&store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let filepath = tools::required_string_param(¶m, "filepath")?.to_owned(); @@ -1220,7 +1236,7 @@ fn pxar_file_download( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); @@ -1346,14 +1362,14 @@ fn get_notes( ) -> Result { let datastore = DataStore::lookup_datastore(&store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } let manifest = datastore.load_manifest_json(&backup_dir)?; @@ -1399,14 +1415,14 @@ fn set_notes( ) -> Result<(), Error> { let datastore = DataStore::lookup_datastore(&store)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; - if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; } let mut manifest = datastore.load_manifest_json(&backup_dir)?; diff --git a/src/api2/admin/sync.rs b/src/api2/admin/sync.rs index ac2858f4..f06625f6 100644 --- a/src/api2/admin/sync.rs +++ b/src/api2/admin/sync.rs @@ -1,6 +1,7 @@ +use std::collections::HashMap; + use anyhow::{Error}; use serde_json::Value; -use std::collections::HashMap; use proxmox::api::{api, ApiMethod, Router, RpcEnvironment}; use proxmox::api::router::SubdirMap; @@ -92,16 +93,23 @@ async fn run_sync_job( let (config, _digest) = sync::config()?; let sync_job: SyncJobConfig = config.lookup("sync", &id)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let delete = sync_job.remove_vanished.unwrap_or(true); let (client, src_repo, tgt_store) = get_pull_parameters(&sync_job.store, &sync_job.remote, &sync_job.remote_store).await?; - let upid_str = WorkerTask::spawn("syncjob", Some(id.clone()), &username.clone(), false, move |worker| async move { + let upid_str = WorkerTask::spawn("syncjob", Some(id.clone()), userid, false, move |worker| async move { worker.log(format!("sync job '{}' start", &id)); - crate::client::pull::pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, String::from("backup@pam")).await?; + crate::client::pull::pull_store( + &worker, + &client, + &src_repo, + tgt_store.clone(), + delete, + Userid::backup_userid().clone(), + ).await?; worker.log(format!("sync job '{}' end", &id)); diff --git a/src/api2/backup.rs b/src/api2/backup.rs index aafea8fa..4b019007 100644 --- a/src/api2/backup.rs +++ b/src/api2/backup.rs @@ -56,12 +56,12 @@ fn upgrade_to_backup_protocol( async move { let debug = param["debug"].as_bool().unwrap_or(false); - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let store = tools::required_string_param(¶m, "store")?.to_owned(); let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?; + user_info.check_privs(&userid, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?; let datastore = DataStore::lookup_datastore(&store)?; @@ -90,11 +90,11 @@ async move { let backup_group = BackupGroup::new(backup_type, backup_id); // lock backup group to only allow one backup per group at a time - let (owner, _group_guard) = datastore.create_locked_backup_group(&backup_group, &username)?; + let (owner, _group_guard) = datastore.create_locked_backup_group(&backup_group, &userid)?; // permission check - if owner != username { // only the owner is allowed to create additional snapshots - bail!("backup owner check failed ({} != {})", username, owner); + if owner != userid { // only the owner is allowed to create additional snapshots + bail!("backup owner check failed ({} != {})", userid, owner); } let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group, true).unwrap_or(None); @@ -109,9 +109,9 @@ async move { let (path, is_new) = datastore.create_backup_dir(&backup_dir)?; if !is_new { bail!("backup directory already exists."); } - WorkerTask::spawn("backup", Some(worker_id), &username.clone(), true, move |worker| { + WorkerTask::spawn("backup", Some(worker_id), userid.clone(), true, move |worker| { let mut env = BackupEnvironment::new( - env_type, username.clone(), worker.clone(), datastore, backup_dir); + env_type, userid, worker.clone(), datastore, backup_dir); env.debug = debug; env.last_backup = last_backup; diff --git a/src/api2/backup/environment.rs b/src/api2/backup/environment.rs index bc780d20..f8c3f651 100644 --- a/src/api2/backup/environment.rs +++ b/src/api2/backup/environment.rs @@ -9,8 +9,9 @@ use proxmox::tools::digest_to_hex; use proxmox::tools::fs::{replace_file, CreateOptions}; use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; -use crate::server::WorkerTask; +use crate::api2::types::Userid; use crate::backup::*; +use crate::server::WorkerTask; use crate::server::formatter::*; use hyper::{Body, Response}; @@ -100,7 +101,7 @@ impl SharedBackupState { pub struct BackupEnvironment { env_type: RpcEnvironmentType, result_attributes: Value, - user: String, + user: Userid, pub debug: bool, pub formatter: &'static OutputFormatter, pub worker: Arc, @@ -113,7 +114,7 @@ pub struct BackupEnvironment { impl BackupEnvironment { pub fn new( env_type: RpcEnvironmentType, - user: String, + user: Userid, worker: Arc, datastore: Arc, backup_dir: BackupDir, @@ -558,7 +559,7 @@ impl RpcEnvironment for BackupEnvironment { } fn get_user(&self) -> Option { - Some(self.user.clone()) + Some(self.user.to_string()) } } diff --git a/src/api2/config/remote.rs b/src/api2/config/remote.rs index 237e7b14..0f983450 100644 --- a/src/api2/config/remote.rs +++ b/src/api2/config/remote.rs @@ -61,7 +61,7 @@ pub fn list_remotes( schema: DNS_NAME_OR_IP_SCHEMA, }, userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, password: { schema: remote::REMOTE_PASSWORD_SCHEMA, @@ -155,7 +155,7 @@ pub enum DeletableProperty { }, userid: { optional: true, - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, password: { optional: true, @@ -188,7 +188,7 @@ pub fn update_remote( name: String, comment: Option, host: Option, - userid: Option, + userid: Option, password: Option, fingerprint: Option, delete: Option>, diff --git a/src/api2/node.rs b/src/api2/node.rs index 5a3ea0ff..e9af5193 100644 --- a/src/api2/node.rs +++ b/src/api2/node.rs @@ -90,12 +90,12 @@ async fn termproxy( cmd: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let userid = rpcenv + let userid: Userid = rpcenv .get_user() - .ok_or_else(|| format_err!("unknown user"))?; - let (username, realm) = crate::auth::parse_userid(&userid)?; + .ok_or_else(|| format_err!("unknown user"))? + .parse()?; - if realm != "pam" { + if userid.realm() != "pam" { bail!("only pam users can use the console"); } @@ -133,10 +133,11 @@ async fn termproxy( _ => bail!("invalid command"), }; + let username = userid.name().to_owned(); let upid = WorkerTask::spawn( "termproxy", None, - &userid, + userid, false, move |worker| async move { // move inside the worker so that it survives and does not close the port @@ -233,6 +234,7 @@ async fn termproxy( }, )?; + // FIXME: We're returning the user NAME only? Ok(json!({ "user": username, "ticket": ticket, @@ -270,14 +272,14 @@ fn upgrade_to_websocket( rpcenv: Box, ) -> ApiResponseFuture { async move { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let ticket = tools::required_string_param(¶m, "vncticket")?.to_owned(); let port: u16 = tools::required_integer_param(¶m, "port")? as u16; // will be checked again by termproxy tools::ticket::verify_term_ticket( crate::auth_helpers::public_auth_key(), - &username, + &userid, &"/system", port, &ticket, diff --git a/src/api2/node/apt.rs b/src/api2/node/apt.rs index 37bf48d4..04beaa4d 100644 --- a/src/api2/node/apt.rs +++ b/src/api2/node/apt.rs @@ -9,7 +9,7 @@ use proxmox::api::router::{Router, SubdirMap}; use crate::server::WorkerTask; use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; -use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA}; +use crate::api2::types::{APTUpdateInfo, NODE_SCHEMA, Userid, UPID_SCHEMA}; const_regex! { VERSION_EPOCH_REGEX = r"^\d+:"; @@ -233,11 +233,11 @@ pub fn apt_update_database( rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; let quiet = quiet.unwrap_or(API_METHOD_APT_UPDATE_DATABASE_PARAM_DEFAULT_QUIET); - let upid_str = WorkerTask::new_thread("aptupdate", None, &username.clone(), to_stdout, move |worker| { + let upid_str = WorkerTask::new_thread("aptupdate", None, userid, to_stdout, move |worker| { if !quiet { worker.log("starting apt-get update") } // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE diff --git a/src/api2/node/disks.rs b/src/api2/node/disks.rs index 2789142a..eed9e257 100644 --- a/src/api2/node/disks.rs +++ b/src/api2/node/disks.rs @@ -13,7 +13,7 @@ use crate::tools::disks::{ }; use crate::server::WorkerTask; -use crate::api2::types::{UPID_SCHEMA, NODE_SCHEMA, BLOCKDEVICE_NAME_SCHEMA}; +use crate::api2::types::{Userid, UPID_SCHEMA, NODE_SCHEMA, BLOCKDEVICE_NAME_SCHEMA}; pub mod directory; pub mod zfs; @@ -140,7 +140,7 @@ pub fn initialize_disk( let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let info = get_disk_usage_info(&disk, true)?; @@ -149,7 +149,7 @@ pub fn initialize_disk( } let upid_str = WorkerTask::new_thread( - "diskinit", Some(disk.clone()), &username.clone(), to_stdout, move |worker| + "diskinit", Some(disk.clone()), userid, to_stdout, move |worker| { worker.log(format!("initialize disk {}", disk)); diff --git a/src/api2/node/disks/directory.rs b/src/api2/node/disks/directory.rs index f243e8f9..f05b781f 100644 --- a/src/api2/node/disks/directory.rs +++ b/src/api2/node/disks/directory.rs @@ -133,7 +133,7 @@ pub fn create_datastore_disk( let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let info = get_disk_usage_info(&disk, true)?; @@ -142,7 +142,7 @@ pub fn create_datastore_disk( } let upid_str = WorkerTask::new_thread( - "dircreate", Some(name.clone()), &username.clone(), to_stdout, move |worker| + "dircreate", Some(name.clone()), userid, to_stdout, move |worker| { worker.log(format!("create datastore '{}' on disk {}", name, disk)); diff --git a/src/api2/node/disks/zfs.rs b/src/api2/node/disks/zfs.rs index be2a1630..c5d6f6a6 100644 --- a/src/api2/node/disks/zfs.rs +++ b/src/api2/node/disks/zfs.rs @@ -254,7 +254,7 @@ pub fn create_zpool( let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let add_datastore = add_datastore.unwrap_or(false); @@ -314,7 +314,7 @@ pub fn create_zpool( } let upid_str = WorkerTask::new_thread( - "zfscreate", Some(name.clone()), &username.clone(), to_stdout, move |worker| + "zfscreate", Some(name.clone()), userid, to_stdout, move |worker| { worker.log(format!("create {:?} zpool '{}' on devices '{}'", raidlevel, name, devices_text)); diff --git a/src/api2/node/network.rs b/src/api2/node/network.rs index 69ce0df6..7266da05 100644 --- a/src/api2/node/network.rs +++ b/src/api2/node/network.rs @@ -625,9 +625,9 @@ pub async fn reload_network_config( network::assert_ifupdown2_installed()?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; - let upid_str = WorkerTask::spawn("srvreload", Some(String::from("networking")), &username.clone(), true, |_worker| async { + let upid_str = WorkerTask::spawn("srvreload", Some(String::from("networking")), userid, true, |_worker| async { let _ = std::fs::rename(network::NETWORK_INTERFACES_NEW_FILENAME, network::NETWORK_INTERFACES_FILENAME); diff --git a/src/api2/node/tasks.rs b/src/api2/node/tasks.rs index 4d703b18..6917869e 100644 --- a/src/api2/node/tasks.rs +++ b/src/api2/node/tasks.rs @@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader}; use anyhow::{Error}; use serde_json::{json, Value}; -use proxmox::api::{api, Router, RpcEnvironment, Permission, UserInformation}; +use proxmox::api::{api, Router, RpcEnvironment, Permission}; use proxmox::api::router::SubdirMap; use proxmox::{identity, list_subdirs_api_method, sortable}; @@ -84,11 +84,11 @@ async fn get_task_status( let upid = extract_upid(¶m)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; - if username != upid.username { + if userid != upid.userid { let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_AUDIT, false)?; + user_info.check_privs(&userid, &["system", "tasks"], PRIV_SYS_AUDIT, false)?; } let mut result = json!({ @@ -99,7 +99,7 @@ async fn get_task_status( "starttime": upid.starttime, "type": upid.worker_type, "id": upid.worker_id, - "user": upid.username, + "user": upid.userid, }); if crate::server::worker_is_active(&upid).await? { @@ -161,11 +161,11 @@ async fn read_task_log( let upid = extract_upid(¶m)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; - if username != upid.username { + if userid != upid.userid { let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_AUDIT, false)?; + user_info.check_privs(&userid, &["system", "tasks"], PRIV_SYS_AUDIT, false)?; } let test_status = param["test-status"].as_bool().unwrap_or(false); @@ -234,11 +234,11 @@ fn stop_task( let upid = extract_upid(¶m)?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; - if username != upid.username { + if userid != upid.userid { let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_MODIFY, false)?; + user_info.check_privs(&userid, &["system", "tasks"], PRIV_SYS_MODIFY, false)?; } server::abort_worker_async(upid); @@ -281,7 +281,7 @@ fn stop_task( default: false, }, userfilter: { - optional:true, + optional: true, type: String, description: "Only list tasks from this user.", }, @@ -307,9 +307,9 @@ pub fn list_tasks( mut rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["system", "tasks"]); + let user_privs = user_info.lookup_privs(&userid, &["system", "tasks"]); let list_all = (user_privs & PRIV_SYS_AUDIT) != 0; @@ -324,11 +324,11 @@ pub fn list_tasks( let mut count = 0; for info in list { - if !list_all && info.upid.username != username { continue; } + if !list_all && info.upid.userid != userid { continue; } - if let Some(username) = userfilter { - if !info.upid.username.contains(username) { continue; } + if let Some(userid) = userfilter { + if !info.upid.userid.as_str().contains(userid) { continue; } } if let Some(store) = store { diff --git a/src/api2/pull.rs b/src/api2/pull.rs index cf7de524..b0db45c9 100644 --- a/src/api2/pull.rs +++ b/src/api2/pull.rs @@ -18,7 +18,7 @@ use crate::config::{ pub fn check_pull_privs( - username: &str, + userid: &Userid, store: &str, remote: &str, remote_store: &str, @@ -27,11 +27,11 @@ pub fn check_pull_privs( let user_info = CachedUserInfo::new()?; - user_info.check_privs(username, &["datastore", store], PRIV_DATASTORE_BACKUP, false)?; - user_info.check_privs(username, &["remote", remote, remote_store], PRIV_REMOTE_READ, false)?; + user_info.check_privs(userid, &["datastore", store], PRIV_DATASTORE_BACKUP, false)?; + user_info.check_privs(userid, &["remote", remote, remote_store], PRIV_REMOTE_READ, false)?; if delete { - user_info.check_privs(username, &["datastore", store], PRIV_DATASTORE_PRUNE, false)?; + user_info.check_privs(userid, &["datastore", store], PRIV_DATASTORE_PRUNE, false)?; } Ok(()) @@ -99,19 +99,19 @@ async fn pull ( rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let delete = remove_vanished.unwrap_or(true); - check_pull_privs(&username, &store, &remote, &remote_store, delete)?; + check_pull_privs(&userid, &store, &remote, &remote_store, delete)?; let (client, src_repo, tgt_store) = get_pull_parameters(&store, &remote, &remote_store).await?; // fixme: set to_stdout to false? - let upid_str = WorkerTask::spawn("sync", Some(store.clone()), &username.clone(), true, move |worker| async move { + let upid_str = WorkerTask::spawn("sync", Some(store.clone()), userid.clone(), true, move |worker| async move { worker.log(format!("sync datastore '{}' start", store)); - pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, username).await?; + pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, userid).await?; worker.log(format!("sync datastore '{}' end", store)); diff --git a/src/api2/reader.rs b/src/api2/reader.rs index 15ff15d1..83ae616a 100644 --- a/src/api2/reader.rs +++ b/src/api2/reader.rs @@ -55,11 +55,11 @@ fn upgrade_to_backup_reader_protocol( async move { let debug = param["debug"].as_bool().unwrap_or(false); - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let store = tools::required_string_param(¶m, "store")?.to_owned(); let user_info = CachedUserInfo::new()?; - user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_READ, false)?; + user_info.check_privs(&userid, &["datastore", &store], PRIV_DATASTORE_READ, false)?; let datastore = DataStore::lookup_datastore(&store)?; @@ -90,9 +90,14 @@ fn upgrade_to_backup_reader_protocol( let worker_id = format!("{}_{}_{}_{:08X}", store, backup_type, backup_id, backup_dir.backup_time().timestamp()); - WorkerTask::spawn("reader", Some(worker_id), &username.clone(), true, move |worker| { + WorkerTask::spawn("reader", Some(worker_id), userid.clone(), true, move |worker| { let mut env = ReaderEnvironment::new( - env_type, username.clone(), worker.clone(), datastore, backup_dir); + env_type, + userid, + worker.clone(), + datastore, + backup_dir, + ); env.debug = debug; diff --git a/src/api2/reader/environment.rs b/src/api2/reader/environment.rs index 623be36a..87bd5747 100644 --- a/src/api2/reader/environment.rs +++ b/src/api2/reader/environment.rs @@ -5,9 +5,10 @@ use serde_json::{json, Value}; use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; -use crate::server::WorkerTask; +use crate::api2::types::Userid; use crate::backup::*; use crate::server::formatter::*; +use crate::server::WorkerTask; //use proxmox::tools; @@ -16,7 +17,7 @@ use crate::server::formatter::*; pub struct ReaderEnvironment { env_type: RpcEnvironmentType, result_attributes: Value, - user: String, + user: Userid, pub debug: bool, pub formatter: &'static OutputFormatter, pub worker: Arc, @@ -28,7 +29,7 @@ pub struct ReaderEnvironment { impl ReaderEnvironment { pub fn new( env_type: RpcEnvironmentType, - user: String, + user: Userid, worker: Arc, datastore: Arc, backup_dir: BackupDir, @@ -77,7 +78,7 @@ impl RpcEnvironment for ReaderEnvironment { } fn get_user(&self) -> Option { - Some(self.user.clone()) + Some(self.user.to_string()) } } diff --git a/src/api2/status.rs b/src/api2/status.rs index 4f985430..c44e73f1 100644 --- a/src/api2/status.rs +++ b/src/api2/status.rs @@ -10,14 +10,14 @@ use proxmox::api::{ Router, RpcEnvironment, SubdirMap, - UserInformation, }; use crate::api2::types::{ DATASTORE_SCHEMA, RRDMode, RRDTimeFrameResolution, - TaskListItem + TaskListItem, + Userid, }; use crate::server; @@ -84,13 +84,13 @@ fn datastore_status( let (config, _digest) = datastore::config()?; - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; let mut list = Vec::new(); for (store, (_, _)) in &config.sections { - let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]); let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0; if !allowed { continue; @@ -202,9 +202,9 @@ pub fn list_tasks( rpcenv: &mut dyn RpcEnvironment, ) -> Result, Error> { - let username = rpcenv.get_user().unwrap(); + let userid: Userid = rpcenv.get_user().unwrap().parse()?; let user_info = CachedUserInfo::new()?; - let user_privs = user_info.lookup_privs(&username, &["system", "tasks"]); + let user_privs = user_info.lookup_privs(&userid, &["system", "tasks"]); let list_all = (user_privs & PRIV_SYS_AUDIT) != 0; @@ -212,7 +212,7 @@ pub fn list_tasks( let list: Vec = server::read_task_list()? .into_iter() .map(TaskListItem::from) - .filter(|entry| list_all || entry.user == username) + .filter(|entry| list_all || entry.user == userid) .collect(); Ok(list.into()) diff --git a/src/api2/types/macros.rs b/src/api2/types/macros.rs new file mode 100644 index 00000000..ee939ecf --- /dev/null +++ b/src/api2/types/macros.rs @@ -0,0 +1,4 @@ +//! Macros exported from api2::types. + +#[macro_export] +macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") } diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index 22eae220..fb386d46 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -1,5 +1,5 @@ -use anyhow::{bail}; -use ::serde::{Deserialize, Serialize}; +use anyhow::bail; +use serde::{Deserialize, Serialize}; use proxmox::api::{api, schema::*}; use proxmox::const_regex; @@ -7,6 +7,16 @@ use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32}; use crate::backup::CryptMode; +#[macro_use] +mod macros; + +#[macro_use] +mod userid; +pub use userid::{Realm, RealmRef}; +pub use userid::{Username, UsernameRef}; +pub use userid::Userid; +pub use userid::PROXMOX_GROUP_ID_SCHEMA; + // File names: may not contain slashes, may not start with "." pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { if name.starts_with('.') { @@ -21,19 +31,6 @@ pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { macro_rules! DNS_LABEL { () => (r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)") } macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL!())) } -// we only allow a limited set of characters -// colon is not allowed, because we store usernames in -// colon separated lists)! -// slash is not allowed because it is used as pve API delimiter -// also see "man useradd" -macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") } -macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) } - -macro_rules! USER_ID_REGEX_STR { () => (concat!(USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!())) } - -#[macro_export] -macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") } - macro_rules! CIDR_V4_REGEX_STR { () => (concat!(r"(?:", IPV4RE!(), r"/\d{1,2})$")) } macro_rules! CIDR_V6_REGEX_STR { () => (concat!(r"(?:", IPV6RE!(), r"/\d{1,3})$")) } @@ -67,12 +64,8 @@ const_regex!{ pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$"); - pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$"); - pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE!() ,"):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$"); - pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$"); - pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; pub ACL_PATH_REGEX = concat!(r"^(?:/|", r"(?:/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$"); @@ -115,12 +108,6 @@ pub const DNS_NAME_FORMAT: ApiStringFormat = pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&DNS_NAME_OR_IP_REGEX); -pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat = - ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX); - -pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat = - ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX); - pub const PASSWORD_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PASSWORD_REGEX); @@ -343,24 +330,6 @@ pub const DNS_NAME_OR_IP_SCHEMA: Schema = StringSchema::new("DNS name or IP addr .format(&DNS_NAME_OR_IP_FORMAT) .schema(); -pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = StringSchema::new("Authentication domain ID") - .format(&PROXMOX_SAFE_ID_FORMAT) - .min_length(3) - .max_length(32) - .schema(); - -pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID") - .format(&PROXMOX_USER_ID_FORMAT) - .min_length(3) - .max_length(64) - .schema(); - -pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID") - .format(&PROXMOX_GROUP_ID_FORMAT) - .min_length(3) - .max_length(64) - .schema(); - pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name (/sys/block/).") .format(&BLOCKDEVICE_NAME_FORMAT) .min_length(3) @@ -388,6 +357,10 @@ pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name schema: BACKUP_ARCHIVE_NAME_SCHEMA }, }, + owner: { + type: Userid, + optional: true, + }, }, )] #[derive(Serialize, Deserialize)] @@ -403,7 +376,7 @@ pub struct GroupListItem { pub files: Vec, /// The owner of group #[serde(skip_serializing_if="Option::is_none")] - pub owner: Option, + pub owner: Option, } #[api( @@ -422,6 +395,10 @@ pub struct GroupListItem { schema: BACKUP_ARCHIVE_NAME_SCHEMA }, }, + owner: { + type: Userid, + optional: true, + }, }, )] #[derive(Serialize, Deserialize)] @@ -441,7 +418,7 @@ pub struct SnapshotListItem { pub size: Option, /// The owner of the snapshots group #[serde(skip_serializing_if="Option::is_none")] - pub owner: Option, + pub owner: Option, } #[api( @@ -584,7 +561,8 @@ pub struct StorageStatus { #[api( properties: { - "upid": { schema: UPID_SCHEMA }, + upid: { schema: UPID_SCHEMA }, + user: { type: Userid }, }, )] #[derive(Serialize, Deserialize)] @@ -604,7 +582,7 @@ pub struct TaskListItem { /// Worker ID (arbitrary ASCII string) pub worker_id: Option, /// The user who started the task - pub user: String, + pub user: Userid, /// The task end time (Epoch) #[serde(skip_serializing_if="Option::is_none")] pub endtime: Option, @@ -627,7 +605,7 @@ impl From for TaskListItem { starttime: info.upid.starttime, worker_type: info.upid.worker_type, worker_id: info.upid.worker_id, - user: info.upid.username, + user: info.upid.userid, endtime, status, } @@ -893,9 +871,6 @@ fn test_cert_fingerprint_schema() -> Result<(), anyhow::Error> { #[test] fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> { - - let schema = PROXMOX_USER_ID_SCHEMA; - let invalid_user_ids = [ "x", // too short "xx", // too short @@ -909,7 +884,7 @@ fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> { ]; for name in invalid_user_ids.iter() { - if let Ok(_) = parse_simple_value(name, &schema) { + if let Ok(_) = parse_simple_value(name, &Userid::API_SCHEMA) { bail!("test userid '{}' failed - got Ok() while exception an error.", name); } } @@ -923,7 +898,7 @@ fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> { ]; for name in valid_user_ids.iter() { - let v = match parse_simple_value(name, &schema) { + let v = match parse_simple_value(name, &Userid::API_SCHEMA) { Ok(v) => v, Err(err) => { bail!("unable to parse userid '{}' - {}", name, err); diff --git a/src/api2/types/userid.rs b/src/api2/types/userid.rs new file mode 100644 index 00000000..a81559af --- /dev/null +++ b/src/api2/types/userid.rs @@ -0,0 +1,376 @@ +//! Types for user handling. +//! +//! We have [`Username`]s and [`Realm`]s. To uniquely identify a user, they must be combined into a [`Userid`]. +//! +//! Since they're all string types, they're organized as follows: +//! +//! * [`Username`]: an owned user name. Internally a `String`. +//! * [`UsernameRef`]: a borrowed user name. Pairs with a `Username` the same way a `str` pairs +//! with `String`, meaning you can only make references to it. +//! * [`Realm`]: an owned realm (`String` equivalent). +//! * [`RealmRef`]: a borrowed realm (`str` equivalent). +//! * [`Userid`]: an owned user id (`"user@realm"`). Note that this does not have a separte +//! borrowed type. +//! +//! Note that `Username`s are not unique, therefore they do not implement `Eq` and cannot be +//! compared directly. If a direct comparison is really required, they can be compared as strings +//! via the `as_str()` method. [`Realm`]s and [`Userid`]s on the other hand can be compared with +//! each other, as in those two cases the comparison has meaning. + +use std::borrow::Borrow; +use std::convert::TryFrom; +use std::fmt; + +use anyhow::{bail, format_err, Error}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +use proxmox::api::api; +use proxmox::api::schema::{ApiStringFormat, Schema, StringSchema}; +use proxmox::const_regex; + +// we only allow a limited set of characters +// colon is not allowed, because we store usernames in +// colon separated lists)! +// slash is not allowed because it is used as pve API delimiter +// also see "man useradd" +macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") } +macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) } +macro_rules! USER_ID_REGEX_STR { () => (concat!(USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!())) } + +const_regex! { + pub PROXMOX_USER_NAME_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"$"); + pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$"); + pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$"); +} + +pub const PROXMOX_USER_NAME_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&PROXMOX_USER_NAME_REGEX); + +pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX); + +pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX); + +pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID") + .format(&PROXMOX_GROUP_ID_FORMAT) + .min_length(3) + .max_length(64) + .schema(); + +pub const PROXMOX_AUTH_REALM_STRING_SCHEMA: StringSchema = + StringSchema::new("Authentication domain ID") + .format(&super::PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32); +pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = PROXMOX_AUTH_REALM_STRING_SCHEMA.schema(); + + +#[api( + type: String, + format: &PROXMOX_USER_NAME_FORMAT, +)] +/// The user name part of a user id. +/// +/// This alone does NOT uniquely identify the user and therefore does not implement `Eq`. In order +/// to compare user names directly, they need to be explicitly compared as strings by calling +/// `.as_str()`. +#[derive(Clone, Debug, Hash, Deserialize, Serialize)] +pub struct Username(String); + +/// A reference to a user name part of a user id. This alone does NOT uniquely identify the user. +/// +/// This is like a `str` to the `String` of a [`Username`]. +#[derive(Debug, Hash)] +pub struct UsernameRef(str); + +impl UsernameRef { + fn new(s: &str) -> &Self { + unsafe { &*(s as *const str as *const UsernameRef) } + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::ops::Deref for Username { + type Target = UsernameRef; + + fn deref(&self) -> &UsernameRef { + self.borrow() + } +} + +impl Borrow for Username { + fn borrow(&self) -> &UsernameRef { + UsernameRef::new(self.as_str()) + } +} + +impl AsRef for Username { + fn as_ref(&self) -> &UsernameRef { + UsernameRef::new(self.as_str()) + } +} + +impl ToOwned for UsernameRef { + type Owned = Username; + + fn to_owned(&self) -> Self::Owned { + Username(self.0.to_owned()) + } +} + +impl TryFrom for Username { + type Error = Error; + + fn try_from(s: String) -> Result { + if !PROXMOX_USER_NAME_REGEX.is_match(&s) { + bail!("invalid user name"); + } + + Ok(Self(s)) + } +} + +impl<'a> TryFrom<&'a str> for &'a UsernameRef { + type Error = Error; + + fn try_from(s: &'a str) -> Result<&'a UsernameRef, Error> { + if !PROXMOX_USER_NAME_REGEX.is_match(s) { + bail!("invalid name in user id"); + } + + Ok(UsernameRef::new(s)) + } +} + +#[api(schema: PROXMOX_AUTH_REALM_SCHEMA)] +/// An authentication realm. +#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct Realm(String); + +/// A reference to an authentication realm. +/// +/// This is like a `str` to the `String` of a `Realm`. +#[derive(Debug, Hash, Eq, PartialEq)] +pub struct RealmRef(str); + +impl RealmRef { + fn new(s: &str) -> &Self { + unsafe { &*(s as *const str as *const RealmRef) } + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::ops::Deref for Realm { + type Target = RealmRef; + + fn deref(&self) -> &RealmRef { + self.borrow() + } +} + +impl Borrow for Realm { + fn borrow(&self) -> &RealmRef { + RealmRef::new(self.as_str()) + } +} + +impl AsRef for Realm { + fn as_ref(&self) -> &RealmRef { + RealmRef::new(self.as_str()) + } +} + +impl ToOwned for RealmRef { + type Owned = Realm; + + fn to_owned(&self) -> Self::Owned { + Realm(self.0.to_owned()) + } +} + +impl TryFrom for Realm { + type Error = Error; + + fn try_from(s: String) -> Result { + PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&s) + .map_err(|_| format_err!("invalid realm"))?; + + Ok(Self(s)) + } +} + +impl<'a> TryFrom<&'a str> for &'a RealmRef { + type Error = Error; + + fn try_from(s: &'a str) -> Result<&'a RealmRef, Error> { + PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(s) + .map_err(|_| format_err!("invalid realm"))?; + + Ok(RealmRef::new(s)) + } +} + +impl PartialEq for Realm { + fn eq(&self, rhs: &str) -> bool { + self.0 == rhs + } +} + +impl PartialEq<&str> for Realm { + fn eq(&self, rhs: &&str) -> bool { + self.0 == *rhs + } +} + +impl PartialEq for RealmRef { + fn eq(&self, rhs: &str) -> bool { + self.0 == *rhs + } +} + +impl PartialEq<&str> for RealmRef { + fn eq(&self, rhs: &&str) -> bool { + self.0 == **rhs + } +} + +/// A complete user id consting of a user name and a realm. +#[derive(Clone, Debug, Hash)] +pub struct Userid { + data: String, + name_len: usize, + //name: Username, + //realm: Realm, +} + +impl Userid { + pub const API_SCHEMA: Schema = StringSchema::new("User ID") + .format(&PROXMOX_USER_ID_FORMAT) + .min_length(3) + .max_length(64) + .schema(); + + const fn new(data: String, name_len: usize) -> Self { + Self { data, name_len } + } + + pub fn name(&self) -> &UsernameRef { + UsernameRef::new(&self.data[..self.name_len]) + } + + pub fn realm(&self) -> &RealmRef { + RealmRef::new(&self.data[(self.name_len + 1)..]) + } + + pub fn as_str(&self) -> &str { + &self.data + } + + /// Get the "backup@pam" user id. + pub fn backup_userid() -> &'static Self { + &*BACKUP_USERID + } + + /// Get the "root@pam" user id. + pub fn root_userid() -> &'static Self { + &*ROOT_USERID + } +} + +lazy_static! { + pub static ref BACKUP_USERID: Userid = Userid::new("backup@pam".to_string(), 6); + pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4); +} + +impl Eq for Userid {} + +impl PartialEq for Userid { + fn eq(&self, rhs: &Self) -> bool { + self.data == rhs.data && self.name_len == rhs.name_len + } +} + +impl From<(Username, Realm)> for Userid { + fn from(parts: (Username, Realm)) -> Self { + Self::from((parts.0.as_ref(), parts.1.as_ref())) + } +} + +impl From<(&UsernameRef, &RealmRef)> for Userid { + fn from(parts: (&UsernameRef, &RealmRef)) -> Self { + let data = format!("{}@{}", parts.0.as_str(), parts.1.as_str()); + let name_len = parts.0.as_str().len(); + Self { data, name_len } + } +} + +impl fmt::Display for Userid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.data.fmt(f) + } +} + +impl std::str::FromStr for Userid { + type Err = Error; + + fn from_str(id: &str) -> Result { + let (name, realm) = match id.as_bytes().iter().rposition(|&b| b == b'@') { + Some(pos) => (&id[..pos], &id[(pos + 1)..]), + None => bail!("not a valid user id"), + }; + + PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(realm) + .map_err(|_| format_err!("invalid realm in user id"))?; + + Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm)))) + } +} + +impl TryFrom for Userid { + type Error = Error; + + fn try_from(data: String) -> Result { + let name_len = data + .as_bytes() + .iter() + .rposition(|&b| b == b'@') + .ok_or_else(|| format_err!("not a valid user id"))?; + + PROXMOX_AUTH_REALM_STRING_SCHEMA.check_constraints(&data[(name_len + 1)..]) + .map_err(|_| format_err!("invalid realm in user id"))?; + + Ok(Self { data, name_len }) + } +} + +impl PartialEq for Userid { + fn eq(&self, rhs: &str) -> bool { + rhs.len() > self.name_len + 2 // make sure range access below is allowed + && rhs.starts_with(self.name().as_str()) + && rhs.as_bytes()[self.name_len] == b'@' + && &rhs[(self.name_len + 1)..] == self.realm().as_str() + } +} + +impl PartialEq<&str> for Userid { + fn eq(&self, rhs: &&str) -> bool { + *self == **rhs + } +} + +impl PartialEq for Userid { + fn eq(&self, rhs: &String) -> bool { + self == rhs.as_str() + } +} + +proxmox::forward_deserialize_to_from_str!(Userid); +proxmox::forward_serialize_to_display!(Userid); diff --git a/src/auth.rs b/src/auth.rs index 768dd2b8..8f5d0353 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,39 +10,54 @@ use base64; use anyhow::{bail, format_err, Error}; use serde_json::json; +use crate::api2::types::{Userid, UsernameRef, RealmRef}; + pub trait ProxmoxAuthenticator { - fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error>; - fn store_password(&self, username: &str, password: &str) -> Result<(), Error>; + fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error>; + fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>; } pub struct PAM(); impl ProxmoxAuthenticator for PAM { - fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error> { + fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error> { let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap(); - auth.get_handler().set_credentials(username, password); + auth.get_handler().set_credentials(username.as_str(), password); auth.authenticate()?; return Ok(()); } - fn store_password(&self, username: &str, password: &str) -> Result<(), Error> { + fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> { let mut child = Command::new("passwd") - .arg(username) + .arg(username.as_str()) .stdin(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .or_else(|err| Err(format_err!("unable to set password for '{}' - execute passwd failed: {}", username, err)))?; + .map_err(|err| format_err!( + "unable to set password for '{}' - execute passwd failed: {}", + username.as_str(), + err, + ))?; // Note: passwd reads password twice from stdin (for verify) writeln!(child.stdin.as_mut().unwrap(), "{}\n{}", password, password)?; - let output = child.wait_with_output() - .or_else(|err| Err(format_err!("unable to set password for '{}' - wait failed: {}", username, err)))?; + let output = child + .wait_with_output() + .map_err(|err| format_err!( + "unable to set password for '{}' - wait failed: {}", + username.as_str(), + err, + ))?; if !output.status.success() { - bail!("unable to set password for '{}' - {}", username, String::from_utf8_lossy(&output.stderr)); + bail!( + "unable to set password for '{}' - {}", + username.as_str(), + String::from_utf8_lossy(&output.stderr), + ); } Ok(()) @@ -90,23 +105,23 @@ pub fn verify_crypt_pw(password: &str, enc_password: &str) -> Result<(), Error> Ok(()) } -const SHADOW_CONFIG_FILENAME: &str = "/etc/proxmox-backup/shadow.json"; +const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json"); impl ProxmoxAuthenticator for PBS { - fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error> { + fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error> { let data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; - match data[username].as_str() { + match data[username.as_str()].as_str() { None => bail!("no password set"), Some(enc_password) => verify_crypt_pw(password, enc_password)?, } Ok(()) } - fn store_password(&self, username: &str, password: &str) -> Result<(), Error> { + fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> { let enc_password = encrypt_pw(password)?; let mut data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; - data[username] = enc_password.into(); + data[username.as_str()] = enc_password.into(); let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); let options = proxmox::tools::fs::CreateOptions::new() @@ -121,28 +136,18 @@ impl ProxmoxAuthenticator for PBS { } } -pub fn parse_userid(userid: &str) -> Result<(String, String), Error> { - let data: Vec<&str> = userid.rsplitn(2, '@').collect(); - - if data.len() != 2 { - bail!("userid '{}' has no realm", userid); - } - Ok((data[1].to_owned(), data[0].to_owned())) -} - /// Lookup the autenticator for the specified realm -pub fn lookup_authenticator(realm: &str) -> Result, Error> { - match realm { +pub fn lookup_authenticator(realm: &RealmRef) -> Result, Error> { + match realm.as_str() { "pam" => Ok(Box::new(PAM())), "pbs" => Ok(Box::new(PBS())), - _ => bail!("unknown realm '{}'", realm), + _ => bail!("unknown realm '{}'", realm.as_str()), } } /// Authenticate users -pub fn authenticate_user(userid: &str, password: &str) -> Result<(), Error> { - let (username, realm) = parse_userid(userid)?; +pub fn authenticate_user(userid: &Userid, password: &str) -> Result<(), Error> { - lookup_authenticator(&realm)? - .authenticate_user(&username, password) + lookup_authenticator(userid.realm())? + .authenticate_user(userid.name(), password) } diff --git a/src/auth_helpers.rs b/src/auth_helpers.rs index a1212355..54c5d4d8 100644 --- a/src/auth_helpers.rs +++ b/src/auth_helpers.rs @@ -10,16 +10,17 @@ use std::path::PathBuf; use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; use proxmox::try_block; +use crate::api2::types::Userid; use crate::tools::epoch_now_u64; fn compute_csrf_secret_digest( timestamp: i64, secret: &[u8], - username: &str, + userid: &Userid, ) -> String { let mut hasher = sha::Sha256::new(); - let data = format!("{:08X}:{}:", timestamp, username); + let data = format!("{:08X}:{}:", timestamp, userid); hasher.update(data.as_bytes()); hasher.update(secret); @@ -28,19 +29,19 @@ fn compute_csrf_secret_digest( pub fn assemble_csrf_prevention_token( secret: &[u8], - username: &str, + userid: &Userid, ) -> String { let epoch = epoch_now_u64().unwrap() as i64; - let digest = compute_csrf_secret_digest(epoch, secret, username); + let digest = compute_csrf_secret_digest(epoch, secret, userid); format!("{:08X}:{}", epoch, digest) } pub fn verify_csrf_prevention_token( secret: &[u8], - username: &str, + userid: &Userid, token: &str, min_age: i64, max_age: i64, @@ -62,7 +63,7 @@ pub fn verify_csrf_prevention_token( let ttime = i64::from_str_radix(timestamp, 16). map_err(|err| format_err!("timestamp format error - {}", err))?; - let digest = compute_csrf_secret_digest(ttime, secret, username); + let digest = compute_csrf_secret_digest(ttime, secret, userid); if digest != sig { bail!("invalid signature."); diff --git a/src/backup/datastore.rs b/src/backup/datastore.rs index 1565712c..5b6075ec 100644 --- a/src/backup/datastore.rs +++ b/src/backup/datastore.rs @@ -21,7 +21,7 @@ use super::{DataBlob, ArchiveType, archive_type}; use crate::config::datastore; use crate::server::WorkerTask; use crate::tools; -use crate::api2::types::GarbageCollectionStatus; +use crate::api2::types::{GarbageCollectionStatus, Userid}; lazy_static! { static ref DATASTORE_MAP: Mutex>> = Mutex::new(HashMap::new()); @@ -287,16 +287,21 @@ impl DataStore { /// Returns the backup owner. /// /// The backup owner is the user who first created the backup group. - pub fn get_owner(&self, backup_group: &BackupGroup) -> Result { + pub fn get_owner(&self, backup_group: &BackupGroup) -> Result { let mut full_path = self.base_path(); full_path.push(backup_group.group_path()); full_path.push("owner"); let owner = proxmox::tools::fs::file_read_firstline(full_path)?; - Ok(owner.trim_end().to_string()) // remove trailing newline + Ok(owner.trim_end().parse()?) // remove trailing newline } /// Set the backup owner. - pub fn set_owner(&self, backup_group: &BackupGroup, userid: &str, force: bool) -> Result<(), Error> { + pub fn set_owner( + &self, + backup_group: &BackupGroup, + userid: &Userid, + force: bool, + ) -> Result<(), Error> { let mut path = self.base_path(); path.push(backup_group.group_path()); path.push("owner"); @@ -326,8 +331,11 @@ impl DataStore { /// current owner (instead of setting the owner). /// /// This also aquires an exclusive lock on the directory and returns the lock guard. - pub fn create_locked_backup_group(&self, backup_group: &BackupGroup, userid: &str) -> Result<(String, BackupGroupGuard), Error> { - + pub fn create_locked_backup_group( + &self, + backup_group: &BackupGroup, + userid: &Userid, + ) -> Result<(Userid, BackupGroupGuard), Error> { // create intermediate path first: let base_path = self.base_path(); diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index a7056685..c6c11c75 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -184,7 +184,7 @@ pub fn complete_repository(_arg: &str, _param: &HashMap) -> Vec< result } -fn connect(server: &str, userid: &str) -> Result { +fn connect(server: &str, userid: &Userid) -> Result { let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index 7ae0852f..617a0a0e 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -59,12 +59,17 @@ fn connect() -> Result { .verify_cert(false); // not required for connection to localhost let client = if uid.is_root() { - let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some("root@pam"), None)?; + let ticket = assemble_rsa_ticket( + private_auth_key(), + "PBS", + Some(Userid::root_userid()), + None, + )?; options = options.password(Some(ticket)); - HttpClient::new("localhost", "root@pam", options)? + HttpClient::new("localhost", Userid::root_userid(), options)? } else { options = options.ticket_cache(true).interactive(true); - HttpClient::new("localhost", "root@pam", options)? + HttpClient::new("localhost", Userid::root_userid(), options)? }; Ok(client) diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index dc0d16d2..3f7bf3ec 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -9,6 +9,7 @@ use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype}; use proxmox::try_block; use proxmox::api::RpcEnvironmentType; +use proxmox_backup::api2::types::Userid; use proxmox_backup::configdir; use proxmox_backup::buildcfg; use proxmox_backup::server; @@ -318,7 +319,7 @@ async fn schedule_datastore_garbage_collection() { if let Err(err) = WorkerTask::new_thread( worker_type, Some(store.clone()), - "backup@pam", + Userid::backup_userid().clone(), false, move |worker| { worker.log(format!("starting garbage collection on store {}", store)); @@ -429,7 +430,7 @@ async fn schedule_datastore_prune() { if let Err(err) = WorkerTask::new_thread( worker_type, Some(store.clone()), - "backup@pam", + Userid::backup_userid().clone(), false, move |worker| { worker.log(format!("Starting datastore prune on store \"{}\"", store)); @@ -568,14 +569,14 @@ async fn schedule_datastore_sync_jobs() { } }; - let username = String::from("backup@pam"); + let userid = Userid::backup_userid().clone(); let delete = job_config.remove_vanished.unwrap_or(true); if let Err(err) = WorkerTask::spawn( worker_type, Some(job_id.clone()), - &username.clone(), + userid.clone(), false, move |worker| async move { worker.log(format!("Starting datastore sync job '{}'", job_id)); @@ -594,7 +595,7 @@ async fn schedule_datastore_sync_jobs() { let src_repo = BackupRepository::new(Some(remote.userid), Some(remote.host), job_config.remote_store); - pull_store(&worker, &client, &src_repo, tgt_store, delete, username).await?; + pull_store(&worker, &client, &src_repo, tgt_store, delete, userid).await?; Ok(()) } diff --git a/src/client/backup_repo.rs b/src/client/backup_repo.rs index d02a1859..ae40ad2d 100644 --- a/src/client/backup_repo.rs +++ b/src/client/backup_repo.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::fmt; use anyhow::{format_err, Error}; @@ -15,7 +16,7 @@ pub const BACKUP_REPO_URL: ApiStringFormat = ApiStringFormat::Pattern(&BACKUP_RE #[derive(Debug)] pub struct BackupRepository { /// The user name used for Authentication - user: Option, + user: Option, /// The host name or IP address host: Option, /// The name of the datastore @@ -24,15 +25,15 @@ pub struct BackupRepository { impl BackupRepository { - pub fn new(user: Option, host: Option, store: String) -> Self { + pub fn new(user: Option, host: Option, store: String) -> Self { Self { user, host, store } } - pub fn user(&self) -> &str { + pub fn user(&self) -> &Userid { if let Some(ref user) = self.user { - return user; + return &user; } - "root@pam" + Userid::root_userid() } pub fn host(&self) -> &str { @@ -73,7 +74,7 @@ impl std::str::FromStr for BackupRepository { .ok_or_else(|| format_err!("unable to parse repository url '{}'", url))?; Ok(Self { - user: cap.get(1).map(|m| m.as_str().to_owned()), + user: cap.get(1).map(|m| Userid::try_from(m.as_str().to_owned())).transpose()?, host: cap.get(2).map(|m| m.as_str().to_owned()), store: cap[3].to_owned(), }) diff --git a/src/client/http_client.rs b/src/client/http_client.rs index a930ea56..dd457c12 100644 --- a/src/client/http_client.rs +++ b/src/client/http_client.rs @@ -24,6 +24,7 @@ use proxmox::{ }; use super::pipe_to_stream::PipeToSendStream; +use crate::api2::types::Userid; use crate::tools::async_io::EitherStream; use crate::tools::{self, BroadcastFuture, DEFAULT_ENCODE_SET}; @@ -104,7 +105,7 @@ pub struct HttpClient { } /// Delete stored ticket data (logout) -pub fn delete_ticket_info(prefix: &str, server: &str, username: &str) -> Result<(), Error> { +pub fn delete_ticket_info(prefix: &str, server: &str, username: &Userid) -> Result<(), Error> { let base = BaseDirectories::with_prefix(prefix)?; @@ -116,7 +117,7 @@ pub fn delete_ticket_info(prefix: &str, server: &str, username: &str) -> Result< let mut data = file_get_json(&path, Some(json!({})))?; if let Some(map) = data[server].as_object_mut() { - map.remove(username); + map.remove(username.as_str()); } replace_file(path, data.to_string().as_bytes(), CreateOptions::new().perm(mode))?; @@ -223,7 +224,7 @@ fn store_ticket_info(prefix: &str, server: &str, username: &str, ticket: &str, t Ok(()) } -fn load_ticket_info(prefix: &str, server: &str, username: &str) -> Option<(String, String)> { +fn load_ticket_info(prefix: &str, server: &str, userid: &Userid) -> Option<(String, String)> { let base = BaseDirectories::with_prefix(prefix).ok()?; // usually /run/user//... @@ -231,7 +232,7 @@ fn load_ticket_info(prefix: &str, server: &str, username: &str) -> Option<(Strin let data = file_get_json(&path, None).ok()?; let now = Utc::now().timestamp(); let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60; - let uinfo = data[server][username].as_object()?; + let uinfo = data[server][userid.as_str()].as_object()?; let timestamp = uinfo["timestamp"].as_i64()?; let age = now - timestamp; @@ -245,8 +246,11 @@ fn load_ticket_info(prefix: &str, server: &str, username: &str) -> Option<(Strin } impl HttpClient { - - pub fn new(server: &str, username: &str, mut options: HttpClientOptions) -> Result { + pub fn new( + server: &str, + userid: &Userid, + mut options: HttpClientOptions, + ) -> Result { let verified_fingerprint = Arc::new(Mutex::new(None)); @@ -306,20 +310,20 @@ impl HttpClient { } else { let mut ticket_info = None; if use_ticket_cache { - ticket_info = load_ticket_info(options.prefix.as_ref().unwrap(), server, username); + ticket_info = load_ticket_info(options.prefix.as_ref().unwrap(), server, userid); } if let Some((ticket, _token)) = ticket_info { ticket } else { - Self::get_password(&username, options.interactive)? + Self::get_password(userid, options.interactive)? } }; let login_future = Self::credentials( client.clone(), server.to_owned(), - username.to_owned(), - password, + userid.to_owned(), + password.to_owned(), ).map_ok({ let server = server.to_string(); let prefix = options.prefix.clone(); @@ -355,7 +359,7 @@ impl HttpClient { (*self.fingerprint.lock().unwrap()).clone() } - fn get_password(username: &str, interactive: bool) -> Result { + fn get_password(username: &Userid, interactive: bool) -> Result { // If we're on a TTY, query the user for a password if interactive && tty::stdin_isatty() { let msg = format!("Password for \"{}\": ", username); @@ -579,7 +583,7 @@ impl HttpClient { async fn credentials( client: Client, server: String, - username: String, + username: Userid, password: String, ) -> Result { let data = json!({ "username": username, "password": password }); diff --git a/src/client/pull.rs b/src/client/pull.rs index 429ab458..f1dde9b4 100644 --- a/src/client/pull.rs +++ b/src/client/pull.rs @@ -401,7 +401,7 @@ pub async fn pull_store( src_repo: &BackupRepository, tgt_store: Arc, delete: bool, - username: String, + userid: Userid, ) -> Result<(), Error> { // explicit create shared lock to prevent GC on newly created chunks @@ -432,11 +432,11 @@ pub async fn pull_store( for item in list { let group = BackupGroup::new(&item.backup_type, &item.backup_id); - let (owner, _lock_guard) = tgt_store.create_locked_backup_group(&group, &username)?; + let (owner, _lock_guard) = tgt_store.create_locked_backup_group(&group, &userid)?; // permission check - if owner != username { // only the owner is allowed to create additional snapshots + if userid != owner { // only the owner is allowed to create additional snapshots worker.log(format!("sync group {}/{} failed - owner check failed ({} != {})", - item.backup_type, item.backup_id, username, owner)); + item.backup_type, item.backup_id, userid, owner)); errors = true; continue; // do not stop here, instead continue } diff --git a/src/config/acl.rs b/src/config/acl.rs index d5d9c0aa..c5d5dd5f 100644 --- a/src/config/acl.rs +++ b/src/config/acl.rs @@ -15,6 +15,8 @@ use proxmox::tools::{fs::replace_file, fs::CreateOptions}; use proxmox::constnamemap; use proxmox::api::{api, schema::*}; +use crate::api2::types::Userid; + // define Privilege bitfield constnamemap! { @@ -224,7 +226,7 @@ pub struct AclTree { } pub struct AclTreeNode { - pub users: HashMap>, + pub users: HashMap>, pub groups: HashMap>, pub children: BTreeMap, } @@ -239,7 +241,7 @@ impl AclTreeNode { } } - pub fn extract_roles(&self, user: &str, all: bool) -> HashSet { + pub fn extract_roles(&self, user: &Userid, all: bool) -> HashSet { let user_roles = self.extract_user_roles(user, all); if !user_roles.is_empty() { // user privs always override group privs @@ -249,7 +251,7 @@ impl AclTreeNode { self.extract_group_roles(user, all) } - pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet { + pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashSet { let mut set = HashSet::new(); @@ -273,7 +275,7 @@ impl AclTreeNode { set } - pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet { + pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashSet { let mut set = HashSet::new(); @@ -305,7 +307,7 @@ impl AclTreeNode { roles.remove(role); } - pub fn delete_user_role(&mut self, userid: &str, role: &str) { + pub fn delete_user_role(&mut self, userid: &Userid, role: &str) { let roles = match self.users.get_mut(userid) { Some(r) => r, None => return, @@ -324,7 +326,7 @@ impl AclTreeNode { } } - pub fn insert_user_role(&mut self, user: String, role: String, propagate: bool) { + pub fn insert_user_role(&mut self, user: Userid, role: String, propagate: bool) { let map = self.users.entry(user).or_insert_with(|| HashMap::new()); if role == ROLE_NAME_NO_ACCESS { map.clear(); @@ -376,7 +378,7 @@ impl AclTree { node.delete_group_role(group, role); } - pub fn delete_user_role(&mut self, path: &str, userid: &str, role: &str) { + pub fn delete_user_role(&mut self, path: &str, userid: &Userid, role: &str) { let path = split_acl_path(path); let node = match self.get_node(&path) { Some(n) => n, @@ -391,10 +393,10 @@ impl AclTree { node.insert_group_role(group.to_string(), role.to_string(), propagate); } - pub fn insert_user_role(&mut self, path: &str, user: &str, role: &str, propagate: bool) { + pub fn insert_user_role(&mut self, path: &str, user: &Userid, role: &str, propagate: bool) { let path = split_acl_path(path); let node = self.get_or_insert_node(&path); - node.insert_user_role(user.to_string(), role.to_string(), propagate); + node.insert_user_role(user.to_owned(), role.to_string(), propagate); } fn write_node_config( @@ -521,7 +523,7 @@ impl AclTree { let group = &user_or_group[1..]; node.insert_group_role(group.to_string(), role.to_string(), propagate); } else { - node.insert_user_role(user_or_group.to_string(), role.to_string(), propagate); + node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate); } } } @@ -569,7 +571,7 @@ impl AclTree { Ok(tree) } - pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet { + pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashSet { let mut node = &self.root; let mut role_set = node.extract_roles(userid, path.is_empty()); @@ -665,13 +667,14 @@ pub fn save_config(acl: &AclTree) -> Result<(), Error> { #[cfg(test)] mod test { - use anyhow::{Error}; use super::AclTree; + use crate::api2::types::Userid; + fn check_roles( tree: &AclTree, - user: &str, + user: &Userid, path: &str, expected_roles: &str, ) { @@ -686,22 +689,23 @@ mod test { } #[test] - fn test_acl_line_compression() -> Result<(), Error> { + fn test_acl_line_compression() { - let tree = AclTree::from_raw(r###" -acl:0:/store/store2:user1:Admin -acl:0:/store/store2:user2:Admin -acl:0:/store/store2:user1:DatastoreBackup -acl:0:/store/store2:user2:DatastoreBackup -"###)?; + let tree = AclTree::from_raw( + "\ + acl:0:/store/store2:user1@pbs:Admin\n\ + acl:0:/store/store2:user2@pbs:Admin\n\ + acl:0:/store/store2:user1@pbs:DatastoreBackup\n\ + acl:0:/store/store2:user2@pbs:DatastoreBackup\n\ + ", + ) + .expect("failed to parse acl tree"); let mut raw: Vec = Vec::new(); - tree.write_config(&mut raw)?; - let raw = std::str::from_utf8(&raw)?; + tree.write_config(&mut raw).expect("failed to write acl tree"); + let raw = std::str::from_utf8(&raw).expect("acl tree is not valid utf8"); - assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,DatastoreBackup\n"); - - Ok(()) + assert_eq!(raw, "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n"); } #[test] @@ -712,15 +716,17 @@ acl:1:/storage:user1@pbs:Admin acl:1:/storage/store1:user1@pbs:DatastoreBackup acl:1:/storage/store2:user2@pbs:DatastoreBackup "###)?; - check_roles(&tree, "user1@pbs", "/", ""); - check_roles(&tree, "user1@pbs", "/storage", "Admin"); - check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); - check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); + let user1: Userid = "user1@pbs".parse()?; + check_roles(&tree, &user1, "/", ""); + check_roles(&tree, &user1, "/storage", "Admin"); + check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); + check_roles(&tree, &user1, "/storage/store2", "Admin"); - check_roles(&tree, "user2@pbs", "/", ""); - check_roles(&tree, "user2@pbs", "/storage", ""); - check_roles(&tree, "user2@pbs", "/storage/store1", ""); - check_roles(&tree, "user2@pbs", "/storage/store2", "DatastoreBackup"); + let user2: Userid = "user2@pbs".parse()?; + check_roles(&tree, &user2, "/", ""); + check_roles(&tree, &user2, "/storage", ""); + check_roles(&tree, &user2, "/storage/store1", ""); + check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup"); Ok(()) } @@ -733,22 +739,23 @@ acl:1:/:user1@pbs:Admin acl:1:/storage:user1@pbs:NoAccess acl:1:/storage/store1:user1@pbs:DatastoreBackup "###)?; - check_roles(&tree, "user1@pbs", "/", "Admin"); - check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); - check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); - check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess"); - check_roles(&tree, "user1@pbs", "/system", "Admin"); + let user1: Userid = "user1@pbs".parse()?; + check_roles(&tree, &user1, "/", "Admin"); + check_roles(&tree, &user1, "/storage", "NoAccess"); + check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); + check_roles(&tree, &user1, "/storage/store2", "NoAccess"); + check_roles(&tree, &user1, "/system", "Admin"); let tree = AclTree::from_raw(r###" acl:1:/:user1@pbs:Admin acl:0:/storage:user1@pbs:NoAccess acl:1:/storage/store1:user1@pbs:DatastoreBackup "###)?; - check_roles(&tree, "user1@pbs", "/", "Admin"); - check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); - check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); - check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); - check_roles(&tree, "user1@pbs", "/system", "Admin"); + check_roles(&tree, &user1, "/", "Admin"); + check_roles(&tree, &user1, "/storage", "NoAccess"); + check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); + check_roles(&tree, &user1, "/storage/store2", "Admin"); + check_roles(&tree, &user1, "/system", "Admin"); Ok(()) } @@ -758,13 +765,15 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup let mut tree = AclTree::new(); - tree.insert_user_role("/", "user1@pbs", "Admin", true); - tree.insert_user_role("/", "user1@pbs", "Audit", true); + let user1: Userid = "user1@pbs".parse()?; - check_roles(&tree, "user1@pbs", "/", "Admin,Audit"); + tree.insert_user_role("/", &user1, "Admin", true); + tree.insert_user_role("/", &user1, "Audit", true); - tree.insert_user_role("/", "user1@pbs", "NoAccess", true); - check_roles(&tree, "user1@pbs", "/", "NoAccess"); + check_roles(&tree, &user1, "/", "Admin,Audit"); + + tree.insert_user_role("/", &user1, "NoAccess", true); + check_roles(&tree, &user1, "/", "NoAccess"); let mut raw: Vec = Vec::new(); tree.write_config(&mut raw)?; @@ -780,20 +789,21 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup let mut tree = AclTree::new(); - tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true); + let user1: Userid = "user1@pbs".parse()?; - check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); + tree.insert_user_role("/storage", &user1, "NoAccess", true); - tree.insert_user_role("/storage", "user1@pbs", "Admin", true); - tree.insert_user_role("/storage", "user1@pbs", "Audit", true); + check_roles(&tree, &user1, "/storage", "NoAccess"); - check_roles(&tree, "user1@pbs", "/storage", "Admin,Audit"); + tree.insert_user_role("/storage", &user1, "Admin", true); + tree.insert_user_role("/storage", &user1, "Audit", true); - tree.insert_user_role("/storage", "user1@pbs", "NoAccess", true); + check_roles(&tree, &user1, "/storage", "Admin,Audit"); - check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); + tree.insert_user_role("/storage", &user1, "NoAccess", true); + + check_roles(&tree, &user1, "/storage", "NoAccess"); Ok(()) } - } diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs index 56a7c475..2140d9db 100644 --- a/src/config/cached_user_info.rs +++ b/src/config/cached_user_info.rs @@ -10,6 +10,7 @@ use proxmox::api::UserInformation; use super::acl::{AclTree, ROLE_NAMES, ROLE_ADMIN}; use super::user::User; +use crate::api2::types::Userid; /// Cache User/Group/Acl configuration data for fast permission tests pub struct CachedUserInfo { @@ -57,8 +58,8 @@ impl CachedUserInfo { } /// Test if a user account is enabled and not expired - pub fn is_active_user(&self, userid: &str) -> bool { - if let Ok(info) = self.user_cfg.lookup::("user", &userid) { + pub fn is_active_user(&self, userid: &Userid) -> bool { + if let Ok(info) = self.user_cfg.lookup::("user", userid.as_str()) { if !info.enable.unwrap_or(true) { return false; } @@ -77,12 +78,12 @@ impl CachedUserInfo { pub fn check_privs( &self, - userid: &str, + userid: &Userid, path: &[&str], required_privs: u64, partial: bool, ) -> Result<(), Error> { - let user_privs = self.lookup_privs(userid, path); + let user_privs = self.lookup_privs(&userid, path); let allowed = if partial { (user_privs & required_privs) != 0 } else { @@ -97,18 +98,20 @@ impl CachedUserInfo { } } -impl UserInformation for CachedUserInfo { - fn is_superuser(&self, userid: &str) -> bool { +impl CachedUserInfo { + pub fn is_superuser(&self, userid: &Userid) -> bool { userid == "root@pam" } - fn is_group_member(&self, _userid: &str, _group: &str) -> bool { + pub fn is_group_member(&self, _userid: &Userid, _group: &str) -> bool { false } - fn lookup_privs(&self, userid: &str, path: &[&str]) -> u64 { + pub fn lookup_privs(&self, userid: &Userid, path: &[&str]) -> u64 { - if self.is_superuser(userid) { return ROLE_ADMIN; } + if self.is_superuser(userid) { + return ROLE_ADMIN; + } let roles = self.acl_tree.roles(userid, path); let mut privs: u64 = 0; @@ -120,3 +123,20 @@ impl UserInformation for CachedUserInfo { 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, userid: &str, path: &[&str]) -> u64 { + match userid.parse::() { + Ok(userid) => Self::lookup_privs(self, &userid, path), + Err(_) => 0, + } + } +} diff --git a/src/config/remote.rs b/src/config/remote.rs index 23aee1f3..ae2d8957 100644 --- a/src/config/remote.rs +++ b/src/config/remote.rs @@ -40,7 +40,7 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t schema: DNS_NAME_OR_IP_SCHEMA, }, userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, password: { schema: REMOTE_PASSWORD_SCHEMA, @@ -58,7 +58,7 @@ pub struct Remote { #[serde(skip_serializing_if="Option::is_none")] pub comment: Option, pub host: String, - pub userid: String, + pub userid: Userid, #[serde(skip_serializing_if="String::is_empty")] #[serde(with = "proxmox::tools::serde::string_as_base64")] pub password: String, diff --git a/src/config/user.rs b/src/config/user.rs index 2ceea314..b72fa40b 100644 --- a/src/config/user.rs +++ b/src/config/user.rs @@ -56,7 +56,7 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.") #[api( properties: { userid: { - schema: PROXMOX_USER_ID_SCHEMA, + type: Userid, }, comment: { optional: true, @@ -87,7 +87,7 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.") #[derive(Serialize,Deserialize)] /// User properties. pub struct User { - pub userid: String, + pub userid: Userid, #[serde(skip_serializing_if="Option::is_none")] pub comment: Option, #[serde(skip_serializing_if="Option::is_none")] @@ -109,7 +109,7 @@ fn init() -> SectionConfig { }; let plugin = SectionConfigPlugin::new("user".to_string(), Some("userid".to_string()), obj_schema); - let mut config = SectionConfig::new(&PROXMOX_USER_ID_SCHEMA); + let mut config = SectionConfig::new(&Userid::API_SCHEMA); config.register_plugin(plugin); @@ -129,7 +129,7 @@ pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { if data.sections.get("root@pam").is_none() { let user: User = User { - userid: "root@pam".to_string(), + userid: Userid::root_userid().clone(), comment: Some("Superuser".to_string()), enable: None, expire: None, diff --git a/src/server/rest.rs b/src/server/rest.rs index 9547b90e..1e551be5 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -27,6 +27,7 @@ use super::formatter::*; use super::ApiConfig; use crate::auth_helpers::*; +use crate::api2::types::Userid; use crate::tools; use crate::config::cached_user_info::CachedUserInfo; @@ -311,10 +312,10 @@ pub async fn handle_api_request, token: Option, api: &Arc, parts: Parts) -> Response { +fn get_index(userid: Option, token: Option, api: &Arc, parts: Parts) -> Response { let nodename = proxmox::tools::nodename(); - let username = username.unwrap_or_else(|| String::from("")); + let userid = userid.as_ref().map(|u| u.as_str()).unwrap_or(""); let token = token.unwrap_or_else(|| String::from("")); @@ -333,7 +334,7 @@ fn get_index(username: Option, token: Option, api: &Arc, token: &Option, user_info: &CachedUserInfo, -) -> Result { +) -> Result { let ticket_lifetime = tools::ticket::TICKET_LIFETIME; - let username = match ticket { + let userid = match ticket { Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) { - Ok((_age, Some(username))) => username.to_owned(), + Ok((_age, Some(userid))) => userid, Ok((_, None)) => bail!("ticket without username."), Err(err) => return Err(err), } None => bail!("missing ticket"), }; - if !user_info.is_active_user(&username) { + if !user_info.is_active_user(&userid) { bail!("user account disabled or expired."); } if method != hyper::Method::GET { if let Some(token) = token { println!("CSRF prevention token: {:?}", token); - verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?; + verify_csrf_prevention_token(csrf_secret(), &userid, &token, -300, ticket_lifetime)?; } else { bail!("missing CSRF prevention token"); } } - Ok(username) + Ok(userid) } pub async fn handle_request(api: Arc, req: Request) -> Result, Error> { @@ -532,7 +533,7 @@ pub async fn handle_request(api: Arc, req: Request) -> Result rpcenv.set_user(Some(username)), + Ok(userid) => rpcenv.set_user(Some(userid.to_string())), Err(err) => { // always delay unauthorized calls by 3 seconds (from start of request) let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); @@ -580,9 +581,9 @@ pub async fn handle_request(api: Arc, req: Request) -> Result { - let new_token = assemble_csrf_prevention_token(csrf_secret(), &username); - return Ok(get_index(Some(username), Some(new_token), &api, parts)); + Ok(userid) => { + let new_token = assemble_csrf_prevention_token(csrf_secret(), &userid); + return Ok(get_index(Some(userid), Some(new_token), &api, parts)); } _ => { tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; diff --git a/src/server/upid.rs b/src/server/upid.rs index fc837207..e0d7d8af 100644 --- a/src/server/upid.rs +++ b/src/server/upid.rs @@ -1,19 +1,21 @@ -use anyhow::{bail, Error}; -use lazy_static::lazy_static; -use regex::Regex; -use chrono::Local; - use std::sync::atomic::{AtomicUsize, Ordering}; +use anyhow::{bail, Error}; +use chrono::Local; +use lazy_static::lazy_static; +use regex::Regex; + use proxmox::sys::linux::procfs; +use crate::api2::types::Userid; + /// Unique Process/Task Identifier /// /// We use this to uniquely identify worker task. UPIDs have a short /// string repesentaion, which gives additional information about the /// type of the task. for example: /// ```text -/// UPID:{node}:{pid}:{pstart}:{task_id}:{starttime}:{worker_type}:{worker_id}:{username}: +/// UPID:{node}:{pid}:{pstart}:{task_id}:{starttime}:{worker_type}:{worker_id}:{userid}: /// UPID:elsa:00004F37:0039E469:00000000:5CA78B83:garbage_collection::root@pam: /// ``` /// Please note that we use tokio, so a single thread can run multiple @@ -33,7 +35,7 @@ pub struct UPID { /// Worker ID (arbitrary ASCII string) pub worker_id: Option, /// The user who started the task - pub username: String, + pub userid: Userid, /// The node name. pub node: String, } @@ -41,7 +43,11 @@ pub struct UPID { impl UPID { /// Create a new UPID - pub fn new(worker_type: &str, worker_id: Option, username: &str) -> Result { + pub fn new( + worker_type: &str, + worker_id: Option, + userid: Userid, + ) -> Result { let pid = unsafe { libc::getpid() }; @@ -67,7 +73,7 @@ impl UPID { task_id, worker_type: worker_type.to_owned(), worker_id, - username: username.to_owned(), + userid, node: proxmox::tools::nodename().to_owned(), }) } @@ -91,7 +97,7 @@ impl std::str::FromStr for UPID { static ref REGEX: Regex = Regex::new(concat!( r"^UPID:(?P[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P[0-9A-Fa-f]{8}):", r"(?P[0-9A-Fa-f]{8,9}):(?P[0-9A-Fa-f]{8,16}):(?P[0-9A-Fa-f]{8}):", - r"(?P[^:\s]+):(?P[^:\s]*):(?P[^:\s]+):$" + r"(?P[^:\s]+):(?P[^:\s]*):(?P[^:\s]+):$" )).unwrap(); } @@ -104,7 +110,7 @@ impl std::str::FromStr for UPID { task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(), worker_type: cap["wtype"].to_string(), worker_id: if cap["wid"].is_empty() { None } else { Some(cap["wid"].to_string()) }, - username: cap["username"].to_string(), + userid: cap["userid"].parse()?, node: cap["node"].to_string(), }) } else { @@ -124,6 +130,6 @@ impl std::fmt::Display for UPID { // more that 8 characters for pstart write!(f, "UPID:{}:{:08X}:{:08X}:{:08X}:{:08X}:{}:{}:{}:", - self.node, self.pid, self.pstart, self.task_id, self.starttime, self.worker_type, wid, self.username) + self.node, self.pid, self.pstart, self.task_id, self.starttime, self.worker_type, wid, self.userid) } } diff --git a/src/server/worker_task.rs b/src/server/worker_task.rs index f0e28488..852f3ecc 100644 --- a/src/server/worker_task.rs +++ b/src/server/worker_task.rs @@ -20,6 +20,7 @@ use proxmox::tools::fs::{create_path, open_file_locked, replace_file, CreateOpti use super::UPID; use crate::tools::FileLogger; +use crate::api2::types::Userid; macro_rules! PROXMOX_BACKUP_VAR_RUN_DIR_M { () => ("/run/proxmox-backup") } macro_rules! PROXMOX_BACKUP_LOG_DIR_M { () => ("/var/log/proxmox-backup") } @@ -394,10 +395,10 @@ impl Drop for WorkerTask { impl WorkerTask { - pub fn new(worker_type: &str, worker_id: Option, username: &str, to_stdout: bool) -> Result, Error> { + pub fn new(worker_type: &str, worker_id: Option, userid: Userid, to_stdout: bool) -> Result, Error> { println!("register worker"); - let upid = UPID::new(worker_type, worker_id, username)?; + let upid = UPID::new(worker_type, worker_id, userid)?; let task_id = upid.task_id; let mut path = std::path::PathBuf::from(PROXMOX_BACKUP_TASK_DIR); @@ -442,14 +443,14 @@ impl WorkerTask { pub fn spawn( worker_type: &str, worker_id: Option, - username: &str, + userid: Userid, to_stdout: bool, f: F, ) -> Result where F: Send + 'static + FnOnce(Arc) -> T, T: Send + 'static + Future>, { - let worker = WorkerTask::new(worker_type, worker_id, username, to_stdout)?; + let worker = WorkerTask::new(worker_type, worker_id, userid, to_stdout)?; let upid_str = worker.upid.to_string(); let f = f(worker.clone()); tokio::spawn(async move { @@ -464,7 +465,7 @@ impl WorkerTask { pub fn new_thread( worker_type: &str, worker_id: Option, - username: &str, + userid: Userid, to_stdout: bool, f: F, ) -> Result @@ -474,7 +475,7 @@ impl WorkerTask { let (p, c) = oneshot::channel::<()>(); - let worker = WorkerTask::new(worker_type, worker_id, username, to_stdout)?; + let worker = WorkerTask::new(worker_type, worker_id, userid, to_stdout)?; let upid_str = worker.upid.to_string(); let _child = std::thread::Builder::new().name(upid_str.clone()).spawn(move || { diff --git a/src/tools/ticket.rs b/src/tools/ticket.rs index ff639ce1..82fa1101 100644 --- a/src/tools/ticket.rs +++ b/src/tools/ticket.rs @@ -7,6 +7,7 @@ use openssl::pkey::{PKey, Public, Private}; use openssl::sign::{Signer, Verifier}; use openssl::hash::MessageDigest; +use crate::api2::types::Userid; use crate::tools::epoch_now_u64; pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours @@ -15,7 +16,7 @@ const TERM_PREFIX: &str = "PBSTERM"; pub fn assemble_term_ticket( keypair: &PKey, - username: &str, + userid: &Userid, path: &str, port: u16, ) -> Result { @@ -23,22 +24,22 @@ pub fn assemble_term_ticket( keypair, TERM_PREFIX, None, - Some(&format!("{}{}{}", username, path, port)), + Some(&format!("{}{}{}", userid, path, port)), ) } pub fn verify_term_ticket( keypair: &PKey, - username: &str, + userid: &Userid, path: &str, port: u16, ticket: &str, -) -> Result<(i64, Option), Error> { +) -> Result<(i64, Option), Error> { verify_rsa_ticket( keypair, TERM_PREFIX, ticket, - Some(&format!("{}{}{}", username, path, port)), + Some(&format!("{}{}{}", userid, path, port)), -300, TICKET_LIFETIME, ) @@ -47,7 +48,7 @@ pub fn verify_term_ticket( pub fn assemble_rsa_ticket( keypair: &PKey, prefix: &str, - data: Option<&str>, + data: Option<&Userid>, secret_data: Option<&str>, ) -> Result { @@ -59,7 +60,8 @@ pub fn assemble_rsa_ticket( plain.push(':'); if let Some(data) = data { - plain.push_str(data); + use std::fmt::Write; + write!(plain, "{}", data)?; plain.push(':'); } @@ -87,7 +89,7 @@ pub fn verify_rsa_ticket( secret_data: Option<&str>, min_age: i64, max_age: i64, -) -> Result<(i64, Option), Error> { +) -> Result<(i64, Option), Error> { use std::collections::VecDeque; @@ -145,5 +147,5 @@ pub fn verify_rsa_ticket( bail!("invalid ticket - timestamp too old."); } - Ok((age, data)) + Ok((age, data.map(|s| s.parse()).transpose()?)) } diff --git a/tests/worker-task-abort.rs b/tests/worker-task-abort.rs index 519333b1..360d17da 100644 --- a/tests/worker-task-abort.rs +++ b/tests/worker-task-abort.rs @@ -54,21 +54,27 @@ fn worker_task_abort() -> Result<(), Error> { } let errmsg = errmsg1.clone(); - let res = server::WorkerTask::new_thread("garbage_collection", None, "root@pam", true, move |worker| { - println!("WORKER {}", worker); + let res = server::WorkerTask::new_thread( + "garbage_collection", + None, + proxmox_backup::api2::types::Userid::root_userid().clone(), + true, + move |worker| { + println!("WORKER {}", worker); - let result = garbage_collection(&worker); - tools::request_shutdown(); + let result = garbage_collection(&worker); + tools::request_shutdown(); - if let Err(err) = result { - println!("got expected error: {}", err); - } else { - let mut data = errmsg.lock().unwrap(); - *data = Some(String::from("thread finished - seems abort did not work as expected")); - } + if let Err(err) = result { + println!("got expected error: {}", err); + } else { + let mut data = errmsg.lock().unwrap(); + *data = Some(String::from("thread finished - seems abort did not work as expected")); + } - Ok(()) - }); + Ok(()) + }, + ); match res { Err(err) => {