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 <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2020-08-06 15:46:01 +02:00
parent 27d864210a
commit e7cb4dc50d
42 changed files with 877 additions and 417 deletions

View File

@ -4,6 +4,7 @@ use anyhow::{Error};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use proxmox_backup::api2::types::Userid;
use proxmox_backup::client::{HttpClient, HttpClientOptions, BackupReader}; use proxmox_backup::client::{HttpClient, HttpClientOptions, BackupReader};
pub struct DummyWriter { pub struct DummyWriter {
@ -27,7 +28,7 @@ async fn run() -> Result<(), Error> {
let host = "localhost"; let host = "localhost";
let username = "root@pam"; let username = Userid::root_userid();
let options = HttpClientOptions::new() let options = HttpClientOptions::new()
.interactive(true) .interactive(true)

View File

@ -1,5 +1,6 @@
use anyhow::{Error}; use anyhow::{Error};
use proxmox_backup::api2::types::Userid;
use proxmox_backup::client::*; use proxmox_backup::client::*;
async fn upload_speed() -> Result<f64, Error> { async fn upload_speed() -> Result<f64, Error> {
@ -7,7 +8,7 @@ async fn upload_speed() -> Result<f64, Error> {
let host = "localhost"; let host = "localhost";
let datastore = "store2"; let datastore = "store2";
let username = "root@pam"; let username = Userid::root_userid();
let options = HttpClientOptions::new() let options = HttpClientOptions::new()
.interactive(true) .interactive(true)

View File

@ -2,7 +2,7 @@ use anyhow::{bail, format_err, Error};
use serde_json::{json, Value}; 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::api::router::{Router, SubdirMap};
use proxmox::{sortable, identity}; use proxmox::{sortable, identity};
use proxmox::{http_err, list_subdirs_api_method}; 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 /// returns Ok(true) if a ticket has to be created
/// and Ok(false) if not /// and Ok(false) if not
fn authenticate_user( fn authenticate_user(
username: &str, userid: &Userid,
password: &str, password: &str,
path: Option<String>, path: Option<String>,
privs: Option<String>, privs: Option<String>,
@ -31,7 +31,7 @@ fn authenticate_user(
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let user_info = CachedUserInfo::new()?; 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."); bail!("user account disabled or expired.");
} }
@ -39,10 +39,10 @@ fn authenticate_user(
if password.starts_with("PBS:") { 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 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); return Ok(true);
} else { } else {
bail!("ticket login failed - wrong username"); bail!("ticket login failed - wrong userid");
} }
} }
} else if password.starts_with("PBSTERM:") { } else if password.starts_with("PBSTERM:") {
@ -55,7 +55,7 @@ fn authenticate_user(
let port = port.unwrap(); let port = port.unwrap();
if let Ok((_age, _data)) = 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 { for (name, privilege) in PRIVILEGES {
if *name == privilege_name { 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); 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) Ok(true)
} }
@ -83,7 +83,7 @@ fn authenticate_user(
input: { input: {
properties: { properties: {
username: { username: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
password: { password: {
schema: PASSWORD_SCHEMA, schema: PASSWORD_SCHEMA,
@ -130,7 +130,7 @@ fn authenticate_user(
/// ///
/// Returns: An authentication ticket with additional infos. /// Returns: An authentication ticket with additional infos.
fn create_ticket( fn create_ticket(
username: String, username: Userid,
password: String, password: String,
path: Option<String>, path: Option<String>,
privs: Option<String>, privs: Option<String>,
@ -165,7 +165,7 @@ fn create_ticket(
input: { input: {
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
password: { password: {
schema: PASSWORD_SCHEMA, schema: PASSWORD_SCHEMA,
@ -183,13 +183,15 @@ fn create_ticket(
/// Each user is allowed to change his own password. Superuser /// Each user is allowed to change his own password. Superuser
/// can change all passwords. /// can change all passwords.
fn change_password( fn change_password(
userid: String, userid: Userid,
password: String, password: String,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let current_user = rpcenv.get_user() let current_user: Userid = rpcenv
.ok_or_else(|| format_err!("unknown user"))?; .get_user()
.ok_or_else(|| format_err!("unknown user"))?
.parse()?;
let mut allowed = userid == current_user; let mut allowed = userid == current_user;
@ -205,9 +207,8 @@ fn change_password(
bail!("you are not authorized to change the password."); bail!("you are not authorized to change the password.");
} }
let (username, realm) = crate::auth::parse_userid(&userid)?; let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
let authenticator = crate::auth::lookup_authenticator(&realm)?; authenticator.store_password(userid.name(), &password)?;
authenticator.store_password(&username, &password)?;
Ok(Value::Null) Ok(Value::Null)
} }

View File

@ -142,7 +142,7 @@ pub fn read_acl(
}, },
userid: { userid: {
optional: true, optional: true,
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
group: { group: {
optional: true, optional: true,
@ -168,7 +168,7 @@ pub fn update_acl(
path: String, path: String,
role: String, role: String,
propagate: Option<bool>, propagate: Option<bool>,
userid: Option<String>, userid: Option<Userid>,
group: Option<String>, group: Option<String>,
delete: Option<bool>, delete: Option<bool>,
digest: Option<String>, digest: Option<String>,
@ -193,7 +193,7 @@ pub fn update_acl(
} else if let Some(ref userid) = userid { } else if let Some(ref userid) = userid {
if !delete { // Note: we allow to delete non-existent users if !delete { // Note: we allow to delete non-existent users
let user_cfg = crate::config::user::cached_config()?; 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."); bail!("no such user.");
} }
} }

View File

@ -49,7 +49,7 @@ pub fn list_users(
input: { input: {
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
comment: { comment: {
schema: SINGLE_LINE_COMMENT_SCHEMA, schema: SINGLE_LINE_COMMENT_SCHEMA,
@ -94,19 +94,18 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
let (mut config, _digest) = user::config()?; 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); bail!("user '{}' already exists.", user.userid);
} }
let (username, realm) = crate::auth::parse_userid(&user.userid)?; let authenticator = crate::auth::lookup_authenticator(&user.userid.realm())?;
let authenticator = crate::auth::lookup_authenticator(&realm)?;
config.set_data(&user.userid, "user", &user)?; config.set_data(user.userid.as_str(), "user", &user)?;
user::save_config(&config)?; user::save_config(&config)?;
if let Some(password) = password { if let Some(password) = password {
authenticator.store_password(&username, &password)?; authenticator.store_password(user.userid.name(), &password)?;
} }
Ok(()) Ok(())
@ -116,7 +115,7 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
input: { input: {
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
}, },
}, },
@ -129,9 +128,9 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
}, },
)] )]
/// Read user configuration data. /// Read user configuration data.
pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<user::User, Error> { pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<user::User, Error> {
let (config, digest) = user::config()?; 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(); rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(user) Ok(user)
} }
@ -141,7 +140,7 @@ pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
input: { input: {
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
comment: { comment: {
optional: true, optional: true,
@ -183,7 +182,7 @@ pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
)] )]
/// Update user configuration. /// Update user configuration.
pub fn update_user( pub fn update_user(
userid: String, userid: Userid,
comment: Option<String>, comment: Option<String>,
enable: Option<bool>, enable: Option<bool>,
expire: Option<i64>, expire: Option<i64>,
@ -203,7 +202,7 @@ pub fn update_user(
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; 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 { if let Some(comment) = comment {
let comment = comment.trim().to_string(); let comment = comment.trim().to_string();
@ -223,9 +222,8 @@ pub fn update_user(
} }
if let Some(password) = password { if let Some(password) = password {
let (username, realm) = crate::auth::parse_userid(&userid)?; let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
let authenticator = crate::auth::lookup_authenticator(&realm)?; authenticator.store_password(userid.name(), &password)?;
authenticator.store_password(&username, &password)?;
} }
if let Some(firstname) = firstname { if let Some(firstname) = firstname {
@ -239,7 +237,7 @@ pub fn update_user(
data.email = if email.is_empty() { None } else { Some(email) }; 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)?; user::save_config(&config)?;
@ -251,7 +249,7 @@ pub fn update_user(
input: { input: {
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
digest: { digest: {
optional: true, optional: true,
@ -264,7 +262,7 @@ pub fn update_user(
}, },
)] )]
/// Remove a user from the configuration file. /// Remove a user from the configuration file.
pub fn delete_user(userid: String, digest: Option<String>) -> Result<(), Error> { pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> {
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?; 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<String>) -> Result<(), Error>
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
} }
match config.sections.get(&userid) { match config.sections.get(userid.as_str()) {
Some(_) => { config.sections.remove(&userid); }, Some(_) => { config.sections.remove(userid.as_str()); },
None => bail!("user '{}' does not exist.", userid), None => bail!("user '{}' does not exist.", userid),
} }

View File

@ -10,7 +10,8 @@ use serde_json::{json, Value};
use proxmox::api::{ use proxmox::api::{
api, ApiResponseFuture, ApiHandler, ApiMethod, Router, api, ApiResponseFuture, ApiHandler, ApiMethod, Router,
RpcEnvironment, RpcEnvironmentType, Permission, UserInformation}; RpcEnvironment, RpcEnvironmentType, Permission
};
use proxmox::api::router::SubdirMap; use proxmox::api::router::SubdirMap;
use proxmox::api::schema::*; use proxmox::api::schema::*;
use proxmox::tools::fs::{replace_file, CreateOptions}; use proxmox::tools::fs::{replace_file, CreateOptions};
@ -36,7 +37,11 @@ use crate::config::acl::{
PRIV_DATASTORE_BACKUP, 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)?; let owner = store.get_owner(group)?;
if &owner != userid { if &owner != userid {
bail!("backup owner check failed ({} != {})", userid, owner); bail!("backup owner check failed ({} != {})", userid, owner);
@ -44,7 +49,10 @@ fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> R
Ok(()) Ok(())
} }
fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result<(BackupManifest, Vec<BackupContent>), Error> { fn read_backup_index(
store: &DataStore,
backup_dir: &BackupDir,
) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
let (manifest, index_size) = store.load_manifest(backup_dir)?; let (manifest, index_size) = store.load_manifest(backup_dir)?;
@ -131,9 +139,9 @@ fn list_groups(
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<GroupListItem>, Error> { ) -> Result<Vec<GroupListItem>, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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 datastore = DataStore::lookup_datastore(&store)?;
@ -154,7 +162,7 @@ fn list_groups(
let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
let owner = datastore.get_owner(group)?; let owner = datastore.get_owner(group)?;
if !list_all { if !list_all {
if owner != username { continue; } if owner != userid { continue; }
} }
let result_item = GroupListItem { let result_item = GroupListItem {
@ -212,16 +220,16 @@ pub fn list_snapshot_files(
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<BackupContent>, Error> { ) -> Result<Vec<BackupContent>, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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 datastore = DataStore::lookup_datastore(&store)?;
let snapshot = BackupDir::new(backup_type, backup_id, backup_time); let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0; 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)?; let info = BackupInfo::new(&datastore.base_path(), snapshot)?;
@ -264,16 +272,16 @@ fn delete_snapshot(
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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 snapshot = BackupDir::new(backup_type, backup_id, backup_time);
let datastore = DataStore::lookup_datastore(&store)?; let datastore = DataStore::lookup_datastore(&store)?;
let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; 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)?; datastore.remove_backup_dir(&snapshot, false)?;
@ -320,9 +328,9 @@ pub fn list_snapshots (
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<SnapshotListItem>, Error> { ) -> Result<Vec<SnapshotListItem>, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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 datastore = DataStore::lookup_datastore(&store)?;
@ -345,7 +353,7 @@ pub fn list_snapshots (
let owner = datastore.get_owner(group)?; let owner = datastore.get_owner(group)?;
if !list_all { if !list_all {
if owner != username { continue; } if owner != userid { continue; }
} }
let mut size = None; let mut size = None;
@ -481,12 +489,15 @@ pub fn verify(
_ => bail!("parameters do not spefify a backup group or snapshot"), _ => 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 to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
let upid_str = WorkerTask::new_thread( 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 failed_dirs = if let Some(backup_dir) = backup_dir {
let mut verified_chunks = HashSet::with_capacity(1024*16); let mut verified_chunks = HashSet::with_capacity(1024*16);
let mut corrupt_chunks = HashSet::with_capacity(64); let mut corrupt_chunks = HashSet::with_capacity(64);
@ -508,7 +519,8 @@ pub fn verify(
bail!("verfication failed - please check the log for details"); bail!("verfication failed - please check the log for details");
} }
Ok(()) Ok(())
})?; },
)?;
Ok(json!(upid_str)) Ok(json!(upid_str))
} }
@ -593,9 +605,9 @@ fn prune(
let backup_type = tools::required_string_param(&param, "backup-type")?; let backup_type = tools::required_string_param(&param, "backup-type")?;
let backup_id = tools::required_string_param(&param, "backup-id")?; let backup_id = tools::required_string_param(&param, "backup-id")?;
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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); let dry_run = param["dry-run"].as_bool().unwrap_or(false);
@ -604,7 +616,7 @@ fn prune(
let datastore = DataStore::lookup_datastore(&store)?; let datastore = DataStore::lookup_datastore(&store)?;
let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; 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 { let prune_options = PruneOptions {
keep_last: param["keep-last"].as_u64(), 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 // 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! { let result = try_block! {
if keep_all { if keep_all {
@ -728,11 +740,15 @@ fn start_garbage_collection(
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
let upid_str = WorkerTask::new_thread( 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)); worker.log(format!("starting garbage collection on store {}", store));
datastore.garbage_collection(&worker) datastore.garbage_collection(&worker)
})?; },
)?;
Ok(json!(upid_str)) Ok(json!(upid_str))
} }
@ -796,13 +812,13 @@ fn get_datastore_list(
let (config, _digest) = datastore::config()?; 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 user_info = CachedUserInfo::new()?;
let mut list = Vec::new(); let mut list = Vec::new();
for (store, (_, data)) in &config.sections { 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; let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
if allowed { if allowed {
let mut entry = json!({ "store": store }); let mut entry = json!({ "store": store });
@ -847,9 +863,9 @@ fn download_file(
let store = tools::required_string_param(&param, "store")?; let store = tools::required_string_param(&param, "store")?;
let datastore = DataStore::lookup_datastore(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_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(&param, "file-name")?.to_owned(); let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
@ -860,7 +876,7 @@ fn download_file(
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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); println!("Download {} from {} ({}/{})", file_name, store, backup_dir, file_name);
@ -920,9 +936,9 @@ fn download_file_decoded(
let store = tools::required_string_param(&param, "store")?; let store = tools::required_string_param(&param, "store")?;
let datastore = DataStore::lookup_datastore(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_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(&param, "file-name")?.to_owned(); let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
@ -933,7 +949,7 @@ fn download_file_decoded(
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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)?; let (_manifest, files) = read_backup_index(&datastore, &backup_dir)?;
for file in files { for file in files {
@ -1038,8 +1054,8 @@ fn upload_backup_log(
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
check_backup_owner(&datastore, backup_dir.group(), &username)?; check_backup_owner(&datastore, backup_dir.group(), &userid)?;
let mut path = datastore.base_path(); let mut path = datastore.base_path();
path.push(backup_dir.relative_path()); path.push(backup_dir.relative_path());
@ -1108,14 +1124,14 @@ fn catalog(
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let datastore = DataStore::lookup_datastore(&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_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 backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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(); let mut path = datastore.base_path();
path.push(backup_dir.relative_path()); path.push(backup_dir.relative_path());
@ -1207,9 +1223,9 @@ fn pxar_file_download(
let store = tools::required_string_param(&param, "store")?; let store = tools::required_string_param(&param, "store")?;
let datastore = DataStore::lookup_datastore(&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_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(&param, "filepath")?.to_owned(); let filepath = tools::required_string_param(&param, "filepath")?.to_owned();
@ -1220,7 +1236,7 @@ fn pxar_file_download(
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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(); let mut path = datastore.base_path();
path.push(backup_dir.relative_path()); path.push(backup_dir.relative_path());
@ -1346,14 +1362,14 @@ fn get_notes(
) -> Result<String, Error> { ) -> Result<String, Error> {
let datastore = DataStore::lookup_datastore(&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_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 backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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)?; let manifest = datastore.load_manifest_json(&backup_dir)?;
@ -1399,14 +1415,14 @@ fn set_notes(
) -> Result<(), Error> { ) -> Result<(), Error> {
let datastore = DataStore::lookup_datastore(&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_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 backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; 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)?; let mut manifest = datastore.load_manifest_json(&backup_dir)?;

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use anyhow::{Error}; use anyhow::{Error};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment}; use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::router::SubdirMap; use proxmox::api::router::SubdirMap;
@ -92,16 +93,23 @@ async fn run_sync_job(
let (config, _digest) = sync::config()?; let (config, _digest) = sync::config()?;
let sync_job: SyncJobConfig = config.lookup("sync", &id)?; 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 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 (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)); 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)); worker.log(format!("sync job '{}' end", &id));

View File

@ -56,12 +56,12 @@ fn upgrade_to_backup_protocol(
async move { async move {
let debug = param["debug"].as_bool().unwrap_or(false); 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(&param, "store")?.to_owned(); let store = tools::required_string_param(&param, "store")?.to_owned();
let user_info = CachedUserInfo::new()?; 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)?; let datastore = DataStore::lookup_datastore(&store)?;
@ -90,11 +90,11 @@ async move {
let backup_group = BackupGroup::new(backup_type, backup_id); let backup_group = BackupGroup::new(backup_type, backup_id);
// lock backup group to only allow one backup per group at a time // 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 // permission check
if owner != username { // only the owner is allowed to create additional snapshots if owner != userid { // only the owner is allowed to create additional snapshots
bail!("backup owner check failed ({} != {})", username, owner); bail!("backup owner check failed ({} != {})", userid, owner);
} }
let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group, true).unwrap_or(None); 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)?; let (path, is_new) = datastore.create_backup_dir(&backup_dir)?;
if !is_new { bail!("backup directory already exists."); } 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( 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.debug = debug;
env.last_backup = last_backup; env.last_backup = last_backup;

View File

@ -9,8 +9,9 @@ use proxmox::tools::digest_to_hex;
use proxmox::tools::fs::{replace_file, CreateOptions}; use proxmox::tools::fs::{replace_file, CreateOptions};
use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
use crate::server::WorkerTask; use crate::api2::types::Userid;
use crate::backup::*; use crate::backup::*;
use crate::server::WorkerTask;
use crate::server::formatter::*; use crate::server::formatter::*;
use hyper::{Body, Response}; use hyper::{Body, Response};
@ -100,7 +101,7 @@ impl SharedBackupState {
pub struct BackupEnvironment { pub struct BackupEnvironment {
env_type: RpcEnvironmentType, env_type: RpcEnvironmentType,
result_attributes: Value, result_attributes: Value,
user: String, user: Userid,
pub debug: bool, pub debug: bool,
pub formatter: &'static OutputFormatter, pub formatter: &'static OutputFormatter,
pub worker: Arc<WorkerTask>, pub worker: Arc<WorkerTask>,
@ -113,7 +114,7 @@ pub struct BackupEnvironment {
impl BackupEnvironment { impl BackupEnvironment {
pub fn new( pub fn new(
env_type: RpcEnvironmentType, env_type: RpcEnvironmentType,
user: String, user: Userid,
worker: Arc<WorkerTask>, worker: Arc<WorkerTask>,
datastore: Arc<DataStore>, datastore: Arc<DataStore>,
backup_dir: BackupDir, backup_dir: BackupDir,
@ -558,7 +559,7 @@ impl RpcEnvironment for BackupEnvironment {
} }
fn get_user(&self) -> Option<String> { fn get_user(&self) -> Option<String> {
Some(self.user.clone()) Some(self.user.to_string())
} }
} }

View File

@ -61,7 +61,7 @@ pub fn list_remotes(
schema: DNS_NAME_OR_IP_SCHEMA, schema: DNS_NAME_OR_IP_SCHEMA,
}, },
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
password: { password: {
schema: remote::REMOTE_PASSWORD_SCHEMA, schema: remote::REMOTE_PASSWORD_SCHEMA,
@ -155,7 +155,7 @@ pub enum DeletableProperty {
}, },
userid: { userid: {
optional: true, optional: true,
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
password: { password: {
optional: true, optional: true,
@ -188,7 +188,7 @@ pub fn update_remote(
name: String, name: String,
comment: Option<String>, comment: Option<String>,
host: Option<String>, host: Option<String>,
userid: Option<String>, userid: Option<Userid>,
password: Option<String>, password: Option<String>,
fingerprint: Option<String>, fingerprint: Option<String>,
delete: Option<Vec<DeletableProperty>>, delete: Option<Vec<DeletableProperty>>,

View File

@ -90,12 +90,12 @@ async fn termproxy(
cmd: Option<String>, cmd: Option<String>,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let userid = rpcenv let userid: Userid = rpcenv
.get_user() .get_user()
.ok_or_else(|| format_err!("unknown user"))?; .ok_or_else(|| format_err!("unknown user"))?
let (username, realm) = crate::auth::parse_userid(&userid)?; .parse()?;
if realm != "pam" { if userid.realm() != "pam" {
bail!("only pam users can use the console"); bail!("only pam users can use the console");
} }
@ -133,10 +133,11 @@ async fn termproxy(
_ => bail!("invalid command"), _ => bail!("invalid command"),
}; };
let username = userid.name().to_owned();
let upid = WorkerTask::spawn( let upid = WorkerTask::spawn(
"termproxy", "termproxy",
None, None,
&userid, userid,
false, false,
move |worker| async move { move |worker| async move {
// move inside the worker so that it survives and does not close the port // 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!({ Ok(json!({
"user": username, "user": username,
"ticket": ticket, "ticket": ticket,
@ -270,14 +272,14 @@ fn upgrade_to_websocket(
rpcenv: Box<dyn RpcEnvironment>, rpcenv: Box<dyn RpcEnvironment>,
) -> ApiResponseFuture { ) -> ApiResponseFuture {
async move { async move {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let ticket = tools::required_string_param(&param, "vncticket")?.to_owned(); let ticket = tools::required_string_param(&param, "vncticket")?.to_owned();
let port: u16 = tools::required_integer_param(&param, "port")? as u16; let port: u16 = tools::required_integer_param(&param, "port")? as u16;
// will be checked again by termproxy // will be checked again by termproxy
tools::ticket::verify_term_ticket( tools::ticket::verify_term_ticket(
crate::auth_helpers::public_auth_key(), crate::auth_helpers::public_auth_key(),
&username, &userid,
&"/system", &"/system",
port, port,
&ticket, &ticket,

View File

@ -9,7 +9,7 @@ use proxmox::api::router::{Router, SubdirMap};
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; 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! { const_regex! {
VERSION_EPOCH_REGEX = r"^\d+:"; VERSION_EPOCH_REGEX = r"^\d+:";
@ -233,11 +233,11 @@ pub fn apt_update_database(
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> { ) -> Result<String, Error> {
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 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 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") } if !quiet { worker.log("starting apt-get update") }
// TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE // TODO: set proxy /etc/apt/apt.conf.d/76pbsproxy like PVE

View File

@ -13,7 +13,7 @@ use crate::tools::disks::{
}; };
use crate::server::WorkerTask; 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 directory;
pub mod zfs; pub mod zfs;
@ -140,7 +140,7 @@ pub fn initialize_disk(
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; 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)?; let info = get_disk_usage_info(&disk, true)?;
@ -149,7 +149,7 @@ pub fn initialize_disk(
} }
let upid_str = WorkerTask::new_thread( 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)); worker.log(format!("initialize disk {}", disk));

View File

@ -133,7 +133,7 @@ pub fn create_datastore_disk(
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; 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)?; let info = get_disk_usage_info(&disk, true)?;
@ -142,7 +142,7 @@ pub fn create_datastore_disk(
} }
let upid_str = WorkerTask::new_thread( 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)); worker.log(format!("create datastore '{}' on disk {}", name, disk));

View File

@ -254,7 +254,7 @@ pub fn create_zpool(
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false }; 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); let add_datastore = add_datastore.unwrap_or(false);
@ -314,7 +314,7 @@ pub fn create_zpool(
} }
let upid_str = WorkerTask::new_thread( 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)); worker.log(format!("create {:?} zpool '{}' on devices '{}'", raidlevel, name, devices_text));

View File

@ -625,9 +625,9 @@ pub async fn reload_network_config(
network::assert_ifupdown2_installed()?; 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); let _ = std::fs::rename(network::NETWORK_INTERFACES_NEW_FILENAME, network::NETWORK_INTERFACES_FILENAME);

View File

@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader};
use anyhow::{Error}; use anyhow::{Error};
use serde_json::{json, Value}; 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::api::router::SubdirMap;
use proxmox::{identity, list_subdirs_api_method, sortable}; use proxmox::{identity, list_subdirs_api_method, sortable};
@ -84,11 +84,11 @@ async fn get_task_status(
let upid = extract_upid(&param)?; let upid = extract_upid(&param)?;
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()?; 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!({ let mut result = json!({
@ -99,7 +99,7 @@ async fn get_task_status(
"starttime": upid.starttime, "starttime": upid.starttime,
"type": upid.worker_type, "type": upid.worker_type,
"id": upid.worker_id, "id": upid.worker_id,
"user": upid.username, "user": upid.userid,
}); });
if crate::server::worker_is_active(&upid).await? { if crate::server::worker_is_active(&upid).await? {
@ -161,11 +161,11 @@ async fn read_task_log(
let upid = extract_upid(&param)?; let upid = extract_upid(&param)?;
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()?; 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); let test_status = param["test-status"].as_bool().unwrap_or(false);
@ -234,11 +234,11 @@ fn stop_task(
let upid = extract_upid(&param)?; let upid = extract_upid(&param)?;
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()?; 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); server::abort_worker_async(upid);
@ -281,7 +281,7 @@ fn stop_task(
default: false, default: false,
}, },
userfilter: { userfilter: {
optional:true, optional: true,
type: String, type: String,
description: "Only list tasks from this user.", description: "Only list tasks from this user.",
}, },
@ -307,9 +307,9 @@ pub fn list_tasks(
mut rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<TaskListItem>, Error> { ) -> Result<Vec<TaskListItem>, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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; let list_all = (user_privs & PRIV_SYS_AUDIT) != 0;
@ -324,11 +324,11 @@ pub fn list_tasks(
let mut count = 0; let mut count = 0;
for info in list { 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 let Some(userid) = userfilter {
if !info.upid.username.contains(username) { continue; } if !info.upid.userid.as_str().contains(userid) { continue; }
} }
if let Some(store) = store { if let Some(store) = store {

View File

@ -18,7 +18,7 @@ use crate::config::{
pub fn check_pull_privs( pub fn check_pull_privs(
username: &str, userid: &Userid,
store: &str, store: &str,
remote: &str, remote: &str,
remote_store: &str, remote_store: &str,
@ -27,11 +27,11 @@ pub fn check_pull_privs(
let user_info = CachedUserInfo::new()?; 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)?;
user_info.check_privs(username, &["remote", remote, remote_store], PRIV_REMOTE_READ, false)?; user_info.check_privs(userid, &["remote", remote, remote_store], PRIV_REMOTE_READ, false)?;
if delete { 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(()) Ok(())
@ -99,19 +99,19 @@ async fn pull (
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> { ) -> Result<String, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let delete = remove_vanished.unwrap_or(true); 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?; let (client, src_repo, tgt_store) = get_pull_parameters(&store, &remote, &remote_store).await?;
// fixme: set to_stdout to false? // 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)); 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)); worker.log(format!("sync datastore '{}' end", store));

View File

@ -55,11 +55,11 @@ fn upgrade_to_backup_reader_protocol(
async move { async move {
let debug = param["debug"].as_bool().unwrap_or(false); 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(&param, "store")?.to_owned(); let store = tools::required_string_param(&param, "store")?.to_owned();
let user_info = CachedUserInfo::new()?; 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)?; 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()); 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( 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; env.debug = debug;

View File

@ -5,9 +5,10 @@ use serde_json::{json, Value};
use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
use crate::server::WorkerTask; use crate::api2::types::Userid;
use crate::backup::*; use crate::backup::*;
use crate::server::formatter::*; use crate::server::formatter::*;
use crate::server::WorkerTask;
//use proxmox::tools; //use proxmox::tools;
@ -16,7 +17,7 @@ use crate::server::formatter::*;
pub struct ReaderEnvironment { pub struct ReaderEnvironment {
env_type: RpcEnvironmentType, env_type: RpcEnvironmentType,
result_attributes: Value, result_attributes: Value,
user: String, user: Userid,
pub debug: bool, pub debug: bool,
pub formatter: &'static OutputFormatter, pub formatter: &'static OutputFormatter,
pub worker: Arc<WorkerTask>, pub worker: Arc<WorkerTask>,
@ -28,7 +29,7 @@ pub struct ReaderEnvironment {
impl ReaderEnvironment { impl ReaderEnvironment {
pub fn new( pub fn new(
env_type: RpcEnvironmentType, env_type: RpcEnvironmentType,
user: String, user: Userid,
worker: Arc<WorkerTask>, worker: Arc<WorkerTask>,
datastore: Arc<DataStore>, datastore: Arc<DataStore>,
backup_dir: BackupDir, backup_dir: BackupDir,
@ -77,7 +78,7 @@ impl RpcEnvironment for ReaderEnvironment {
} }
fn get_user(&self) -> Option<String> { fn get_user(&self) -> Option<String> {
Some(self.user.clone()) Some(self.user.to_string())
} }
} }

View File

@ -10,14 +10,14 @@ use proxmox::api::{
Router, Router,
RpcEnvironment, RpcEnvironment,
SubdirMap, SubdirMap,
UserInformation,
}; };
use crate::api2::types::{ use crate::api2::types::{
DATASTORE_SCHEMA, DATASTORE_SCHEMA,
RRDMode, RRDMode,
RRDTimeFrameResolution, RRDTimeFrameResolution,
TaskListItem TaskListItem,
Userid,
}; };
use crate::server; use crate::server;
@ -84,13 +84,13 @@ fn datastore_status(
let (config, _digest) = datastore::config()?; 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 user_info = CachedUserInfo::new()?;
let mut list = Vec::new(); let mut list = Vec::new();
for (store, (_, _)) in &config.sections { 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; let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
if !allowed { if !allowed {
continue; continue;
@ -202,9 +202,9 @@ pub fn list_tasks(
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<TaskListItem>, Error> { ) -> Result<Vec<TaskListItem>, Error> {
let username = rpcenv.get_user().unwrap(); let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let user_info = CachedUserInfo::new()?; 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; let list_all = (user_privs & PRIV_SYS_AUDIT) != 0;
@ -212,7 +212,7 @@ pub fn list_tasks(
let list: Vec<TaskListItem> = server::read_task_list()? let list: Vec<TaskListItem> = server::read_task_list()?
.into_iter() .into_iter()
.map(TaskListItem::from) .map(TaskListItem::from)
.filter(|entry| list_all || entry.user == username) .filter(|entry| list_all || entry.user == userid)
.collect(); .collect();
Ok(list.into()) Ok(list.into())

4
src/api2/types/macros.rs Normal file
View File

@ -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._\-]*)") }

View File

@ -1,5 +1,5 @@
use anyhow::{bail}; use anyhow::bail;
use ::serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use proxmox::api::{api, schema::*}; use proxmox::api::{api, schema::*};
use proxmox::const_regex; use proxmox::const_regex;
@ -7,6 +7,16 @@ use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32};
use crate::backup::CryptMode; 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 "." // File names: may not contain slashes, may not start with "."
pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
if name.starts_with('.') { 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_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!())) } 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_V4_REGEX_STR { () => (concat!(r"(?:", IPV4RE!(), r"/\d{1,2})$")) }
macro_rules! CIDR_V6_REGEX_STR { () => (concat!(r"(?:", IPV6RE!(), r"/\d{1,3})$")) } 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 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 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 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")$"); 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 = pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&DNS_NAME_OR_IP_REGEX); 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 = pub const PASSWORD_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PASSWORD_REGEX); 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) .format(&DNS_NAME_OR_IP_FORMAT)
.schema(); .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/<name>).") pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name (/sys/block/<name>).")
.format(&BLOCKDEVICE_NAME_FORMAT) .format(&BLOCKDEVICE_NAME_FORMAT)
.min_length(3) .min_length(3)
@ -388,6 +357,10 @@ pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name
schema: BACKUP_ARCHIVE_NAME_SCHEMA schema: BACKUP_ARCHIVE_NAME_SCHEMA
}, },
}, },
owner: {
type: Userid,
optional: true,
},
}, },
)] )]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -403,7 +376,7 @@ pub struct GroupListItem {
pub files: Vec<String>, pub files: Vec<String>,
/// The owner of group /// The owner of group
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub owner: Option<String>, pub owner: Option<Userid>,
} }
#[api( #[api(
@ -422,6 +395,10 @@ pub struct GroupListItem {
schema: BACKUP_ARCHIVE_NAME_SCHEMA schema: BACKUP_ARCHIVE_NAME_SCHEMA
}, },
}, },
owner: {
type: Userid,
optional: true,
},
}, },
)] )]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -441,7 +418,7 @@ pub struct SnapshotListItem {
pub size: Option<u64>, pub size: Option<u64>,
/// The owner of the snapshots group /// The owner of the snapshots group
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub owner: Option<String>, pub owner: Option<Userid>,
} }
#[api( #[api(
@ -584,7 +561,8 @@ pub struct StorageStatus {
#[api( #[api(
properties: { properties: {
"upid": { schema: UPID_SCHEMA }, upid: { schema: UPID_SCHEMA },
user: { type: Userid },
}, },
)] )]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -604,7 +582,7 @@ pub struct TaskListItem {
/// Worker ID (arbitrary ASCII string) /// Worker ID (arbitrary ASCII string)
pub worker_id: Option<String>, pub worker_id: Option<String>,
/// The user who started the task /// The user who started the task
pub user: String, pub user: Userid,
/// The task end time (Epoch) /// The task end time (Epoch)
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub endtime: Option<i64>, pub endtime: Option<i64>,
@ -627,7 +605,7 @@ impl From<crate::server::TaskListInfo> for TaskListItem {
starttime: info.upid.starttime, starttime: info.upid.starttime,
worker_type: info.upid.worker_type, worker_type: info.upid.worker_type,
worker_id: info.upid.worker_id, worker_id: info.upid.worker_id,
user: info.upid.username, user: info.upid.userid,
endtime, endtime,
status, status,
} }
@ -893,9 +871,6 @@ fn test_cert_fingerprint_schema() -> Result<(), anyhow::Error> {
#[test] #[test]
fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> { fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> {
let schema = PROXMOX_USER_ID_SCHEMA;
let invalid_user_ids = [ let invalid_user_ids = [
"x", // too short "x", // too short
"xx", // 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() { 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); 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() { 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, Ok(v) => v,
Err(err) => { Err(err) => {
bail!("unable to parse userid '{}' - {}", name, err); bail!("unable to parse userid '{}' - {}", name, err);

376
src/api2/types/userid.rs Normal file
View File

@ -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<UsernameRef> for Username {
fn borrow(&self) -> &UsernameRef {
UsernameRef::new(self.as_str())
}
}
impl AsRef<UsernameRef> 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<String> for Username {
type Error = Error;
fn try_from(s: String) -> Result<Self, Error> {
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<RealmRef> for Realm {
fn borrow(&self) -> &RealmRef {
RealmRef::new(self.as_str())
}
}
impl AsRef<RealmRef> 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<String> for Realm {
type Error = Error;
fn try_from(s: String) -> Result<Self, Error> {
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<str> 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<str> 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<Self, Error> {
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<String> for Userid {
type Error = Error;
fn try_from(data: String) -> Result<Self, Error> {
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<str> 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<String> 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);

View File

@ -10,39 +10,54 @@ use base64;
use anyhow::{bail, format_err, Error}; use anyhow::{bail, format_err, Error};
use serde_json::json; use serde_json::json;
use crate::api2::types::{Userid, UsernameRef, RealmRef};
pub trait ProxmoxAuthenticator { pub trait ProxmoxAuthenticator {
fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error>; fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
fn store_password(&self, username: &str, password: &str) -> Result<(), Error>; fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
} }
pub struct PAM(); pub struct PAM();
impl ProxmoxAuthenticator for 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(); 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()?; auth.authenticate()?;
return Ok(()); 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") let mut child = Command::new("passwd")
.arg(username) .arg(username.as_str())
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .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) // Note: passwd reads password twice from stdin (for verify)
writeln!(child.stdin.as_mut().unwrap(), "{}\n{}", password, password)?; writeln!(child.stdin.as_mut().unwrap(), "{}\n{}", password, password)?;
let output = child.wait_with_output() let output = child
.or_else(|err| Err(format_err!("unable to set password for '{}' - wait failed: {}", username, err)))?; .wait_with_output()
.map_err(|err| format_err!(
"unable to set password for '{}' - wait failed: {}",
username.as_str(),
err,
))?;
if !output.status.success() { 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(()) Ok(())
@ -90,23 +105,23 @@ pub fn verify_crypt_pw(password: &str, enc_password: &str) -> Result<(), Error>
Ok(()) Ok(())
} }
const SHADOW_CONFIG_FILENAME: &str = "/etc/proxmox-backup/shadow.json"; const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
impl ProxmoxAuthenticator for PBS { 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!({})))?; 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"), None => bail!("no password set"),
Some(enc_password) => verify_crypt_pw(password, enc_password)?, Some(enc_password) => verify_crypt_pw(password, enc_password)?,
} }
Ok(()) 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 enc_password = encrypt_pw(password)?;
let mut data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?; 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 mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
let options = proxmox::tools::fs::CreateOptions::new() 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 /// Lookup the autenticator for the specified realm
pub fn lookup_authenticator(realm: &str) -> Result<Box<dyn ProxmoxAuthenticator>, Error> { pub fn lookup_authenticator(realm: &RealmRef) -> Result<Box<dyn ProxmoxAuthenticator>, Error> {
match realm { match realm.as_str() {
"pam" => Ok(Box::new(PAM())), "pam" => Ok(Box::new(PAM())),
"pbs" => Ok(Box::new(PBS())), "pbs" => Ok(Box::new(PBS())),
_ => bail!("unknown realm '{}'", realm), _ => bail!("unknown realm '{}'", realm.as_str()),
} }
} }
/// Authenticate users /// Authenticate users
pub fn authenticate_user(userid: &str, password: &str) -> Result<(), Error> { pub fn authenticate_user(userid: &Userid, password: &str) -> Result<(), Error> {
let (username, realm) = parse_userid(userid)?;
lookup_authenticator(&realm)? lookup_authenticator(userid.realm())?
.authenticate_user(&username, password) .authenticate_user(userid.name(), password)
} }

View File

@ -10,16 +10,17 @@ use std::path::PathBuf;
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
use proxmox::try_block; use proxmox::try_block;
use crate::api2::types::Userid;
use crate::tools::epoch_now_u64; use crate::tools::epoch_now_u64;
fn compute_csrf_secret_digest( fn compute_csrf_secret_digest(
timestamp: i64, timestamp: i64,
secret: &[u8], secret: &[u8],
username: &str, userid: &Userid,
) -> String { ) -> String {
let mut hasher = sha::Sha256::new(); 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(data.as_bytes());
hasher.update(secret); hasher.update(secret);
@ -28,19 +29,19 @@ fn compute_csrf_secret_digest(
pub fn assemble_csrf_prevention_token( pub fn assemble_csrf_prevention_token(
secret: &[u8], secret: &[u8],
username: &str, userid: &Userid,
) -> String { ) -> String {
let epoch = epoch_now_u64().unwrap() as i64; 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) format!("{:08X}:{}", epoch, digest)
} }
pub fn verify_csrf_prevention_token( pub fn verify_csrf_prevention_token(
secret: &[u8], secret: &[u8],
username: &str, userid: &Userid,
token: &str, token: &str,
min_age: i64, min_age: i64,
max_age: i64, max_age: i64,
@ -62,7 +63,7 @@ pub fn verify_csrf_prevention_token(
let ttime = i64::from_str_radix(timestamp, 16). let ttime = i64::from_str_radix(timestamp, 16).
map_err(|err| format_err!("timestamp format error - {}", err))?; 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 { if digest != sig {
bail!("invalid signature."); bail!("invalid signature.");

View File

@ -21,7 +21,7 @@ use super::{DataBlob, ArchiveType, archive_type};
use crate::config::datastore; use crate::config::datastore;
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::tools; use crate::tools;
use crate::api2::types::GarbageCollectionStatus; use crate::api2::types::{GarbageCollectionStatus, Userid};
lazy_static! { lazy_static! {
static ref DATASTORE_MAP: Mutex<HashMap<String, Arc<DataStore>>> = Mutex::new(HashMap::new()); static ref DATASTORE_MAP: Mutex<HashMap<String, Arc<DataStore>>> = Mutex::new(HashMap::new());
@ -287,16 +287,21 @@ impl DataStore {
/// Returns the backup owner. /// Returns the backup owner.
/// ///
/// The backup owner is the user who first created the backup group. /// The backup owner is the user who first created the backup group.
pub fn get_owner(&self, backup_group: &BackupGroup) -> Result<String, Error> { pub fn get_owner(&self, backup_group: &BackupGroup) -> Result<Userid, Error> {
let mut full_path = self.base_path(); let mut full_path = self.base_path();
full_path.push(backup_group.group_path()); full_path.push(backup_group.group_path());
full_path.push("owner"); full_path.push("owner");
let owner = proxmox::tools::fs::file_read_firstline(full_path)?; 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. /// 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(); let mut path = self.base_path();
path.push(backup_group.group_path()); path.push(backup_group.group_path());
path.push("owner"); path.push("owner");
@ -326,8 +331,11 @@ impl DataStore {
/// current owner (instead of setting the owner). /// current owner (instead of setting the owner).
/// ///
/// This also aquires an exclusive lock on the directory and returns the lock guard. /// 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: // create intermediate path first:
let base_path = self.base_path(); let base_path = self.base_path();

View File

@ -184,7 +184,7 @@ pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<
result result
} }
fn connect(server: &str, userid: &str) -> Result<HttpClient, Error> { fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> {
let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();

View File

@ -59,12 +59,17 @@ fn connect() -> Result<HttpClient, Error> {
.verify_cert(false); // not required for connection to localhost .verify_cert(false); // not required for connection to localhost
let client = if uid.is_root() { 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)); options = options.password(Some(ticket));
HttpClient::new("localhost", "root@pam", options)? HttpClient::new("localhost", Userid::root_userid(), options)?
} else { } else {
options = options.ticket_cache(true).interactive(true); options = options.ticket_cache(true).interactive(true);
HttpClient::new("localhost", "root@pam", options)? HttpClient::new("localhost", Userid::root_userid(), options)?
}; };
Ok(client) Ok(client)

View File

@ -9,6 +9,7 @@ use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};
use proxmox::try_block; use proxmox::try_block;
use proxmox::api::RpcEnvironmentType; use proxmox::api::RpcEnvironmentType;
use proxmox_backup::api2::types::Userid;
use proxmox_backup::configdir; use proxmox_backup::configdir;
use proxmox_backup::buildcfg; use proxmox_backup::buildcfg;
use proxmox_backup::server; use proxmox_backup::server;
@ -318,7 +319,7 @@ async fn schedule_datastore_garbage_collection() {
if let Err(err) = WorkerTask::new_thread( if let Err(err) = WorkerTask::new_thread(
worker_type, worker_type,
Some(store.clone()), Some(store.clone()),
"backup@pam", Userid::backup_userid().clone(),
false, false,
move |worker| { move |worker| {
worker.log(format!("starting garbage collection on store {}", store)); 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( if let Err(err) = WorkerTask::new_thread(
worker_type, worker_type,
Some(store.clone()), Some(store.clone()),
"backup@pam", Userid::backup_userid().clone(),
false, false,
move |worker| { move |worker| {
worker.log(format!("Starting datastore prune on store \"{}\"", store)); 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); let delete = job_config.remove_vanished.unwrap_or(true);
if let Err(err) = WorkerTask::spawn( if let Err(err) = WorkerTask::spawn(
worker_type, worker_type,
Some(job_id.clone()), Some(job_id.clone()),
&username.clone(), userid.clone(),
false, false,
move |worker| async move { move |worker| async move {
worker.log(format!("Starting datastore sync job '{}'", job_id)); 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); 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(()) Ok(())
} }

View File

@ -1,3 +1,4 @@
use std::convert::TryFrom;
use std::fmt; use std::fmt;
use anyhow::{format_err, Error}; use anyhow::{format_err, Error};
@ -15,7 +16,7 @@ pub const BACKUP_REPO_URL: ApiStringFormat = ApiStringFormat::Pattern(&BACKUP_RE
#[derive(Debug)] #[derive(Debug)]
pub struct BackupRepository { pub struct BackupRepository {
/// The user name used for Authentication /// The user name used for Authentication
user: Option<String>, user: Option<Userid>,
/// The host name or IP address /// The host name or IP address
host: Option<String>, host: Option<String>,
/// The name of the datastore /// The name of the datastore
@ -24,15 +25,15 @@ pub struct BackupRepository {
impl BackupRepository { impl BackupRepository {
pub fn new(user: Option<String>, host: Option<String>, store: String) -> Self { pub fn new(user: Option<Userid>, host: Option<String>, store: String) -> Self {
Self { user, host, store } Self { user, host, store }
} }
pub fn user(&self) -> &str { pub fn user(&self) -> &Userid {
if let Some(ref user) = self.user { if let Some(ref user) = self.user {
return user; return &user;
} }
"root@pam" Userid::root_userid()
} }
pub fn host(&self) -> &str { 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_or_else(|| format_err!("unable to parse repository url '{}'", url))?;
Ok(Self { 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()), host: cap.get(2).map(|m| m.as_str().to_owned()),
store: cap[3].to_owned(), store: cap[3].to_owned(),
}) })

View File

@ -24,6 +24,7 @@ use proxmox::{
}; };
use super::pipe_to_stream::PipeToSendStream; use super::pipe_to_stream::PipeToSendStream;
use crate::api2::types::Userid;
use crate::tools::async_io::EitherStream; use crate::tools::async_io::EitherStream;
use crate::tools::{self, BroadcastFuture, DEFAULT_ENCODE_SET}; use crate::tools::{self, BroadcastFuture, DEFAULT_ENCODE_SET};
@ -104,7 +105,7 @@ pub struct HttpClient {
} }
/// Delete stored ticket data (logout) /// 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)?; 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!({})))?; let mut data = file_get_json(&path, Some(json!({})))?;
if let Some(map) = data[server].as_object_mut() { 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))?; 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(()) 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()?; let base = BaseDirectories::with_prefix(prefix).ok()?;
// usually /run/user/<uid>/... // usually /run/user/<uid>/...
@ -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 data = file_get_json(&path, None).ok()?;
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60; 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 timestamp = uinfo["timestamp"].as_i64()?;
let age = now - timestamp; let age = now - timestamp;
@ -245,8 +246,11 @@ fn load_ticket_info(prefix: &str, server: &str, username: &str) -> Option<(Strin
} }
impl HttpClient { impl HttpClient {
pub fn new(
pub fn new(server: &str, username: &str, mut options: HttpClientOptions) -> Result<Self, Error> { server: &str,
userid: &Userid,
mut options: HttpClientOptions,
) -> Result<Self, Error> {
let verified_fingerprint = Arc::new(Mutex::new(None)); let verified_fingerprint = Arc::new(Mutex::new(None));
@ -306,20 +310,20 @@ impl HttpClient {
} else { } else {
let mut ticket_info = None; let mut ticket_info = None;
if use_ticket_cache { 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 { if let Some((ticket, _token)) = ticket_info {
ticket ticket
} else { } else {
Self::get_password(&username, options.interactive)? Self::get_password(userid, options.interactive)?
} }
}; };
let login_future = Self::credentials( let login_future = Self::credentials(
client.clone(), client.clone(),
server.to_owned(), server.to_owned(),
username.to_owned(), userid.to_owned(),
password, password.to_owned(),
).map_ok({ ).map_ok({
let server = server.to_string(); let server = server.to_string();
let prefix = options.prefix.clone(); let prefix = options.prefix.clone();
@ -355,7 +359,7 @@ impl HttpClient {
(*self.fingerprint.lock().unwrap()).clone() (*self.fingerprint.lock().unwrap()).clone()
} }
fn get_password(username: &str, interactive: bool) -> Result<String, Error> { fn get_password(username: &Userid, interactive: bool) -> Result<String, Error> {
// If we're on a TTY, query the user for a password // If we're on a TTY, query the user for a password
if interactive && tty::stdin_isatty() { if interactive && tty::stdin_isatty() {
let msg = format!("Password for \"{}\": ", username); let msg = format!("Password for \"{}\": ", username);
@ -579,7 +583,7 @@ impl HttpClient {
async fn credentials( async fn credentials(
client: Client<HttpsConnector>, client: Client<HttpsConnector>,
server: String, server: String,
username: String, username: Userid,
password: String, password: String,
) -> Result<AuthInfo, Error> { ) -> Result<AuthInfo, Error> {
let data = json!({ "username": username, "password": password }); let data = json!({ "username": username, "password": password });

View File

@ -401,7 +401,7 @@ pub async fn pull_store(
src_repo: &BackupRepository, src_repo: &BackupRepository,
tgt_store: Arc<DataStore>, tgt_store: Arc<DataStore>,
delete: bool, delete: bool,
username: String, userid: Userid,
) -> Result<(), Error> { ) -> Result<(), Error> {
// explicit create shared lock to prevent GC on newly created chunks // explicit create shared lock to prevent GC on newly created chunks
@ -432,11 +432,11 @@ pub async fn pull_store(
for item in list { for item in list {
let group = BackupGroup::new(&item.backup_type, &item.backup_id); 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 // 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 ({} != {})", 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; errors = true;
continue; // do not stop here, instead continue continue; // do not stop here, instead continue
} }

View File

@ -15,6 +15,8 @@ use proxmox::tools::{fs::replace_file, fs::CreateOptions};
use proxmox::constnamemap; use proxmox::constnamemap;
use proxmox::api::{api, schema::*}; use proxmox::api::{api, schema::*};
use crate::api2::types::Userid;
// define Privilege bitfield // define Privilege bitfield
constnamemap! { constnamemap! {
@ -224,7 +226,7 @@ pub struct AclTree {
} }
pub struct AclTreeNode { pub struct AclTreeNode {
pub users: HashMap<String, HashMap<String, bool>>, pub users: HashMap<Userid, HashMap<String, bool>>,
pub groups: HashMap<String, HashMap<String, bool>>, pub groups: HashMap<String, HashMap<String, bool>>,
pub children: BTreeMap<String, AclTreeNode>, pub children: BTreeMap<String, AclTreeNode>,
} }
@ -239,7 +241,7 @@ impl AclTreeNode {
} }
} }
pub fn extract_roles(&self, user: &str, all: bool) -> HashSet<String> { pub fn extract_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
let user_roles = self.extract_user_roles(user, all); let user_roles = self.extract_user_roles(user, all);
if !user_roles.is_empty() { if !user_roles.is_empty() {
// user privs always override group privs // user privs always override group privs
@ -249,7 +251,7 @@ impl AclTreeNode {
self.extract_group_roles(user, all) self.extract_group_roles(user, all)
} }
pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet<String> { pub fn extract_user_roles(&self, user: &Userid, all: bool) -> HashSet<String> {
let mut set = HashSet::new(); let mut set = HashSet::new();
@ -273,7 +275,7 @@ impl AclTreeNode {
set set
} }
pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet<String> { pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashSet<String> {
let mut set = HashSet::new(); let mut set = HashSet::new();
@ -305,7 +307,7 @@ impl AclTreeNode {
roles.remove(role); 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) { let roles = match self.users.get_mut(userid) {
Some(r) => r, Some(r) => r,
None => return, 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()); let map = self.users.entry(user).or_insert_with(|| HashMap::new());
if role == ROLE_NAME_NO_ACCESS { if role == ROLE_NAME_NO_ACCESS {
map.clear(); map.clear();
@ -376,7 +378,7 @@ impl AclTree {
node.delete_group_role(group, role); 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 path = split_acl_path(path);
let node = match self.get_node(&path) { let node = match self.get_node(&path) {
Some(n) => n, Some(n) => n,
@ -391,10 +393,10 @@ impl AclTree {
node.insert_group_role(group.to_string(), role.to_string(), propagate); 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 path = split_acl_path(path);
let node = self.get_or_insert_node(&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( fn write_node_config(
@ -521,7 +523,7 @@ impl AclTree {
let group = &user_or_group[1..]; let group = &user_or_group[1..];
node.insert_group_role(group.to_string(), role.to_string(), propagate); node.insert_group_role(group.to_string(), role.to_string(), propagate);
} else { } 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) Ok(tree)
} }
pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet<String> { pub fn roles(&self, userid: &Userid, path: &[&str]) -> HashSet<String> {
let mut node = &self.root; let mut node = &self.root;
let mut role_set = node.extract_roles(userid, path.is_empty()); 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)] #[cfg(test)]
mod test { mod test {
use anyhow::{Error}; use anyhow::{Error};
use super::AclTree; use super::AclTree;
use crate::api2::types::Userid;
fn check_roles( fn check_roles(
tree: &AclTree, tree: &AclTree,
user: &str, user: &Userid,
path: &str, path: &str,
expected_roles: &str, expected_roles: &str,
) { ) {
@ -686,22 +689,23 @@ mod test {
} }
#[test] #[test]
fn test_acl_line_compression() -> Result<(), Error> { fn test_acl_line_compression() {
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(
acl:0:/store/store2:user1:Admin "\
acl:0:/store/store2:user2:Admin acl:0:/store/store2:user1@pbs:Admin\n\
acl:0:/store/store2:user1:DatastoreBackup acl:0:/store/store2:user2@pbs:Admin\n\
acl:0:/store/store2:user2:DatastoreBackup 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<u8> = Vec::new(); let mut raw: Vec<u8> = Vec::new();
tree.write_config(&mut raw)?; tree.write_config(&mut raw).expect("failed to write acl tree");
let raw = std::str::from_utf8(&raw)?; 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"); assert_eq!(raw, "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n");
Ok(())
} }
#[test] #[test]
@ -712,15 +716,17 @@ acl:1:/storage:user1@pbs:Admin
acl:1:/storage/store1:user1@pbs:DatastoreBackup acl:1:/storage/store1:user1@pbs:DatastoreBackup
acl:1:/storage/store2:user2@pbs:DatastoreBackup acl:1:/storage/store2:user2@pbs:DatastoreBackup
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", ""); let user1: Userid = "user1@pbs".parse()?;
check_roles(&tree, "user1@pbs", "/storage", "Admin"); check_roles(&tree, &user1, "/", "");
check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); check_roles(&tree, &user1, "/storage", "Admin");
check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
check_roles(&tree, &user1, "/storage/store2", "Admin");
check_roles(&tree, "user2@pbs", "/", ""); let user2: Userid = "user2@pbs".parse()?;
check_roles(&tree, "user2@pbs", "/storage", ""); check_roles(&tree, &user2, "/", "");
check_roles(&tree, "user2@pbs", "/storage/store1", ""); check_roles(&tree, &user2, "/storage", "");
check_roles(&tree, "user2@pbs", "/storage/store2", "DatastoreBackup"); check_roles(&tree, &user2, "/storage/store1", "");
check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup");
Ok(()) Ok(())
} }
@ -733,22 +739,23 @@ acl:1:/:user1@pbs:Admin
acl:1:/storage:user1@pbs:NoAccess acl:1:/storage:user1@pbs:NoAccess
acl:1:/storage/store1:user1@pbs:DatastoreBackup acl:1:/storage/store1:user1@pbs:DatastoreBackup
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", "Admin"); let user1: Userid = "user1@pbs".parse()?;
check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); check_roles(&tree, &user1, "/", "Admin");
check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); check_roles(&tree, &user1, "/storage", "NoAccess");
check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess"); check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
check_roles(&tree, "user1@pbs", "/system", "Admin"); check_roles(&tree, &user1, "/storage/store2", "NoAccess");
check_roles(&tree, &user1, "/system", "Admin");
let tree = AclTree::from_raw(r###" let tree = AclTree::from_raw(r###"
acl:1:/:user1@pbs:Admin acl:1:/:user1@pbs:Admin
acl:0:/storage:user1@pbs:NoAccess acl:0:/storage:user1@pbs:NoAccess
acl:1:/storage/store1:user1@pbs:DatastoreBackup acl:1:/storage/store1:user1@pbs:DatastoreBackup
"###)?; "###)?;
check_roles(&tree, "user1@pbs", "/", "Admin"); check_roles(&tree, &user1, "/", "Admin");
check_roles(&tree, "user1@pbs", "/storage", "NoAccess"); check_roles(&tree, &user1, "/storage", "NoAccess");
check_roles(&tree, "user1@pbs", "/storage/store1", "DatastoreBackup"); check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup");
check_roles(&tree, "user1@pbs", "/storage/store2", "Admin"); check_roles(&tree, &user1, "/storage/store2", "Admin");
check_roles(&tree, "user1@pbs", "/system", "Admin"); check_roles(&tree, &user1, "/system", "Admin");
Ok(()) Ok(())
} }
@ -758,13 +765,15 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup
let mut tree = AclTree::new(); let mut tree = AclTree::new();
tree.insert_user_role("/", "user1@pbs", "Admin", true); let user1: Userid = "user1@pbs".parse()?;
tree.insert_user_role("/", "user1@pbs", "Audit", true);
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, "/", "Admin,Audit");
check_roles(&tree, "user1@pbs", "/", "NoAccess");
tree.insert_user_role("/", &user1, "NoAccess", true);
check_roles(&tree, &user1, "/", "NoAccess");
let mut raw: Vec<u8> = Vec::new(); let mut raw: Vec<u8> = Vec::new();
tree.write_config(&mut raw)?; tree.write_config(&mut raw)?;
@ -780,20 +789,21 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup
let mut tree = AclTree::new(); 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); check_roles(&tree, &user1, "/storage", "NoAccess");
tree.insert_user_role("/storage", "user1@pbs", "Audit", true);
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(()) Ok(())
} }
} }

View File

@ -10,6 +10,7 @@ use proxmox::api::UserInformation;
use super::acl::{AclTree, ROLE_NAMES, ROLE_ADMIN}; use super::acl::{AclTree, ROLE_NAMES, ROLE_ADMIN};
use super::user::User; use super::user::User;
use crate::api2::types::Userid;
/// Cache User/Group/Acl configuration data for fast permission tests /// Cache User/Group/Acl configuration data for fast permission tests
pub struct CachedUserInfo { pub struct CachedUserInfo {
@ -57,8 +58,8 @@ impl CachedUserInfo {
} }
/// Test if a user account is enabled and not expired /// Test if a user account is enabled and not expired
pub fn is_active_user(&self, userid: &str) -> bool { pub fn is_active_user(&self, userid: &Userid) -> bool {
if let Ok(info) = self.user_cfg.lookup::<User>("user", &userid) { if let Ok(info) = self.user_cfg.lookup::<User>("user", userid.as_str()) {
if !info.enable.unwrap_or(true) { if !info.enable.unwrap_or(true) {
return false; return false;
} }
@ -77,12 +78,12 @@ impl CachedUserInfo {
pub fn check_privs( pub fn check_privs(
&self, &self,
userid: &str, userid: &Userid,
path: &[&str], path: &[&str],
required_privs: u64, required_privs: u64,
partial: bool, partial: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let user_privs = self.lookup_privs(userid, path); let user_privs = self.lookup_privs(&userid, path);
let allowed = if partial { let allowed = if partial {
(user_privs & required_privs) != 0 (user_privs & required_privs) != 0
} else { } else {
@ -97,18 +98,20 @@ impl CachedUserInfo {
} }
} }
impl UserInformation for CachedUserInfo { impl CachedUserInfo {
fn is_superuser(&self, userid: &str) -> bool { pub fn is_superuser(&self, userid: &Userid) -> bool {
userid == "root@pam" 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 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 roles = self.acl_tree.roles(userid, path);
let mut privs: u64 = 0; let mut privs: u64 = 0;
@ -120,3 +123,20 @@ impl UserInformation for CachedUserInfo {
privs 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::<Userid>() {
Ok(userid) => Self::lookup_privs(self, &userid, path),
Err(_) => 0,
}
}
}

View File

@ -40,7 +40,7 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
schema: DNS_NAME_OR_IP_SCHEMA, schema: DNS_NAME_OR_IP_SCHEMA,
}, },
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
password: { password: {
schema: REMOTE_PASSWORD_SCHEMA, schema: REMOTE_PASSWORD_SCHEMA,
@ -58,7 +58,7 @@ pub struct Remote {
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
pub host: String, pub host: String,
pub userid: String, pub userid: Userid,
#[serde(skip_serializing_if="String::is_empty")] #[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")] #[serde(with = "proxmox::tools::serde::string_as_base64")]
pub password: String, pub password: String,

View File

@ -56,7 +56,7 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.")
#[api( #[api(
properties: { properties: {
userid: { userid: {
schema: PROXMOX_USER_ID_SCHEMA, type: Userid,
}, },
comment: { comment: {
optional: true, optional: true,
@ -87,7 +87,7 @@ pub const EMAIL_SCHEMA: Schema = StringSchema::new("E-Mail Address.")
#[derive(Serialize,Deserialize)] #[derive(Serialize,Deserialize)]
/// User properties. /// User properties.
pub struct User { pub struct User {
pub userid: String, pub userid: Userid,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
#[serde(skip_serializing_if="Option::is_none")] #[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 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); config.register_plugin(plugin);
@ -129,7 +129,7 @@ pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
if data.sections.get("root@pam").is_none() { if data.sections.get("root@pam").is_none() {
let user: User = User { let user: User = User {
userid: "root@pam".to_string(), userid: Userid::root_userid().clone(),
comment: Some("Superuser".to_string()), comment: Some("Superuser".to_string()),
enable: None, enable: None,
expire: None, expire: None,

View File

@ -27,6 +27,7 @@ use super::formatter::*;
use super::ApiConfig; use super::ApiConfig;
use crate::auth_helpers::*; use crate::auth_helpers::*;
use crate::api2::types::Userid;
use crate::tools; use crate::tools;
use crate::config::cached_user_info::CachedUserInfo; use crate::config::cached_user_info::CachedUserInfo;
@ -311,10 +312,10 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
Ok(resp) Ok(resp)
} }
fn get_index(username: Option<String>, token: Option<String>, api: &Arc<ApiConfig>, parts: Parts) -> Response<Body> { fn get_index(userid: Option<Userid>, token: Option<String>, api: &Arc<ApiConfig>, parts: Parts) -> Response<Body> {
let nodename = proxmox::tools::nodename(); 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("")); let token = token.unwrap_or_else(|| String::from(""));
@ -333,7 +334,7 @@ fn get_index(username: Option<String>, token: Option<String>, api: &Arc<ApiConfi
let data = json!({ let data = json!({
"NodeName": nodename, "NodeName": nodename,
"UserName": username, "UserName": userid,
"CSRFPreventionToken": token, "CSRFPreventionToken": token,
"debug": debug, "debug": debug,
}); });
@ -461,33 +462,33 @@ fn check_auth(
ticket: &Option<String>, ticket: &Option<String>,
token: &Option<String>, token: &Option<String>,
user_info: &CachedUserInfo, user_info: &CachedUserInfo,
) -> Result<String, Error> { ) -> Result<Userid, Error> {
let ticket_lifetime = tools::ticket::TICKET_LIFETIME; 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) { 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."), Ok((_, None)) => bail!("ticket without username."),
Err(err) => return Err(err), Err(err) => return Err(err),
} }
None => bail!("missing ticket"), None => bail!("missing ticket"),
}; };
if !user_info.is_active_user(&username) { if !user_info.is_active_user(&userid) {
bail!("user account disabled or expired."); bail!("user account disabled or expired.");
} }
if method != hyper::Method::GET { if method != hyper::Method::GET {
if let Some(token) = token { if let Some(token) = token {
println!("CSRF prevention 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 { } else {
bail!("missing CSRF prevention token"); bail!("missing CSRF prevention token");
} }
} }
Ok(username) Ok(userid)
} }
pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<Response<Body>, Error> { pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<Response<Body>, Error> {
@ -532,7 +533,7 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
} else { } else {
let (ticket, token) = extract_auth_data(&parts.headers); let (ticket, token) = extract_auth_data(&parts.headers);
match check_auth(&method, &ticket, &token, &user_info) { match check_auth(&method, &ticket, &token, &user_info) {
Ok(username) => rpcenv.set_user(Some(username)), Ok(userid) => rpcenv.set_user(Some(userid.to_string())),
Err(err) => { Err(err) => {
// always delay unauthorized calls by 3 seconds (from start of request) // always delay unauthorized calls by 3 seconds (from start of request)
let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err);
@ -580,9 +581,9 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
let (ticket, token) = extract_auth_data(&parts.headers); let (ticket, token) = extract_auth_data(&parts.headers);
if ticket != None { if ticket != None {
match check_auth(&method, &ticket, &token, &user_info) { match check_auth(&method, &ticket, &token, &user_info) {
Ok(username) => { Ok(userid) => {
let new_token = assemble_csrf_prevention_token(csrf_secret(), &username); let new_token = assemble_csrf_prevention_token(csrf_secret(), &userid);
return Ok(get_index(Some(username), Some(new_token), &api, parts)); return Ok(get_index(Some(userid), Some(new_token), &api, parts));
} }
_ => { _ => {
tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;

View File

@ -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 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 proxmox::sys::linux::procfs;
use crate::api2::types::Userid;
/// Unique Process/Task Identifier /// Unique Process/Task Identifier
/// ///
/// We use this to uniquely identify worker task. UPIDs have a short /// We use this to uniquely identify worker task. UPIDs have a short
/// string repesentaion, which gives additional information about the /// string repesentaion, which gives additional information about the
/// type of the task. for example: /// type of the task. for example:
/// ```text /// ```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: /// UPID:elsa:00004F37:0039E469:00000000:5CA78B83:garbage_collection::root@pam:
/// ``` /// ```
/// Please note that we use tokio, so a single thread can run multiple /// 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) /// Worker ID (arbitrary ASCII string)
pub worker_id: Option<String>, pub worker_id: Option<String>,
/// The user who started the task /// The user who started the task
pub username: String, pub userid: Userid,
/// The node name. /// The node name.
pub node: String, pub node: String,
} }
@ -41,7 +43,11 @@ pub struct UPID {
impl UPID { impl UPID {
/// Create a new UPID /// Create a new UPID
pub fn new(worker_type: &str, worker_id: Option<String>, username: &str) -> Result<Self, Error> { pub fn new(
worker_type: &str,
worker_id: Option<String>,
userid: Userid,
) -> Result<Self, Error> {
let pid = unsafe { libc::getpid() }; let pid = unsafe { libc::getpid() };
@ -67,7 +73,7 @@ impl UPID {
task_id, task_id,
worker_type: worker_type.to_owned(), worker_type: worker_type.to_owned(),
worker_id, worker_id,
username: username.to_owned(), userid,
node: proxmox::tools::nodename().to_owned(), node: proxmox::tools::nodename().to_owned(),
}) })
} }
@ -91,7 +97,7 @@ impl std::str::FromStr for UPID {
static ref REGEX: Regex = Regex::new(concat!( static ref REGEX: Regex = Regex::new(concat!(
r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):", r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):", r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<username>[^:\s]+):$" r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<userid>[^:\s]+):$"
)).unwrap(); )).unwrap();
} }
@ -104,7 +110,7 @@ impl std::str::FromStr for UPID {
task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(), task_id: usize::from_str_radix(&cap["task_id"], 16).unwrap(),
worker_type: cap["wtype"].to_string(), worker_type: cap["wtype"].to_string(),
worker_id: if cap["wid"].is_empty() { None } else { Some(cap["wid"].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(), node: cap["node"].to_string(),
}) })
} else { } else {
@ -124,6 +130,6 @@ impl std::fmt::Display for UPID {
// more that 8 characters for pstart // more that 8 characters for pstart
write!(f, "UPID:{}:{:08X}:{:08X}:{:08X}:{:08X}:{}:{}:{}:", 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)
} }
} }

View File

@ -20,6 +20,7 @@ use proxmox::tools::fs::{create_path, open_file_locked, replace_file, CreateOpti
use super::UPID; use super::UPID;
use crate::tools::FileLogger; use crate::tools::FileLogger;
use crate::api2::types::Userid;
macro_rules! PROXMOX_BACKUP_VAR_RUN_DIR_M { () => ("/run/proxmox-backup") } macro_rules! PROXMOX_BACKUP_VAR_RUN_DIR_M { () => ("/run/proxmox-backup") }
macro_rules! PROXMOX_BACKUP_LOG_DIR_M { () => ("/var/log/proxmox-backup") } macro_rules! PROXMOX_BACKUP_LOG_DIR_M { () => ("/var/log/proxmox-backup") }
@ -394,10 +395,10 @@ impl Drop for WorkerTask {
impl WorkerTask { impl WorkerTask {
pub fn new(worker_type: &str, worker_id: Option<String>, username: &str, to_stdout: bool) -> Result<Arc<Self>, Error> { pub fn new(worker_type: &str, worker_id: Option<String>, userid: Userid, to_stdout: bool) -> Result<Arc<Self>, Error> {
println!("register worker"); 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 task_id = upid.task_id;
let mut path = std::path::PathBuf::from(PROXMOX_BACKUP_TASK_DIR); let mut path = std::path::PathBuf::from(PROXMOX_BACKUP_TASK_DIR);
@ -442,14 +443,14 @@ impl WorkerTask {
pub fn spawn<F, T>( pub fn spawn<F, T>(
worker_type: &str, worker_type: &str,
worker_id: Option<String>, worker_id: Option<String>,
username: &str, userid: Userid,
to_stdout: bool, to_stdout: bool,
f: F, f: F,
) -> Result<String, Error> ) -> Result<String, Error>
where F: Send + 'static + FnOnce(Arc<WorkerTask>) -> T, where F: Send + 'static + FnOnce(Arc<WorkerTask>) -> T,
T: Send + 'static + Future<Output = Result<(), Error>>, T: Send + 'static + Future<Output = Result<(), Error>>,
{ {
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 upid_str = worker.upid.to_string();
let f = f(worker.clone()); let f = f(worker.clone());
tokio::spawn(async move { tokio::spawn(async move {
@ -464,7 +465,7 @@ impl WorkerTask {
pub fn new_thread<F>( pub fn new_thread<F>(
worker_type: &str, worker_type: &str,
worker_id: Option<String>, worker_id: Option<String>,
username: &str, userid: Userid,
to_stdout: bool, to_stdout: bool,
f: F, f: F,
) -> Result<String, Error> ) -> Result<String, Error>
@ -474,7 +475,7 @@ impl WorkerTask {
let (p, c) = oneshot::channel::<()>(); 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 upid_str = worker.upid.to_string();
let _child = std::thread::Builder::new().name(upid_str.clone()).spawn(move || { let _child = std::thread::Builder::new().name(upid_str.clone()).spawn(move || {

View File

@ -7,6 +7,7 @@ use openssl::pkey::{PKey, Public, Private};
use openssl::sign::{Signer, Verifier}; use openssl::sign::{Signer, Verifier};
use openssl::hash::MessageDigest; use openssl::hash::MessageDigest;
use crate::api2::types::Userid;
use crate::tools::epoch_now_u64; use crate::tools::epoch_now_u64;
pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours
@ -15,7 +16,7 @@ const TERM_PREFIX: &str = "PBSTERM";
pub fn assemble_term_ticket( pub fn assemble_term_ticket(
keypair: &PKey<Private>, keypair: &PKey<Private>,
username: &str, userid: &Userid,
path: &str, path: &str,
port: u16, port: u16,
) -> Result<String, Error> { ) -> Result<String, Error> {
@ -23,22 +24,22 @@ pub fn assemble_term_ticket(
keypair, keypair,
TERM_PREFIX, TERM_PREFIX,
None, None,
Some(&format!("{}{}{}", username, path, port)), Some(&format!("{}{}{}", userid, path, port)),
) )
} }
pub fn verify_term_ticket( pub fn verify_term_ticket(
keypair: &PKey<Public>, keypair: &PKey<Public>,
username: &str, userid: &Userid,
path: &str, path: &str,
port: u16, port: u16,
ticket: &str, ticket: &str,
) -> Result<(i64, Option<String>), Error> { ) -> Result<(i64, Option<Userid>), Error> {
verify_rsa_ticket( verify_rsa_ticket(
keypair, keypair,
TERM_PREFIX, TERM_PREFIX,
ticket, ticket,
Some(&format!("{}{}{}", username, path, port)), Some(&format!("{}{}{}", userid, path, port)),
-300, -300,
TICKET_LIFETIME, TICKET_LIFETIME,
) )
@ -47,7 +48,7 @@ pub fn verify_term_ticket(
pub fn assemble_rsa_ticket( pub fn assemble_rsa_ticket(
keypair: &PKey<Private>, keypair: &PKey<Private>,
prefix: &str, prefix: &str,
data: Option<&str>, data: Option<&Userid>,
secret_data: Option<&str>, secret_data: Option<&str>,
) -> Result<String, Error> { ) -> Result<String, Error> {
@ -59,7 +60,8 @@ pub fn assemble_rsa_ticket(
plain.push(':'); plain.push(':');
if let Some(data) = data { if let Some(data) = data {
plain.push_str(data); use std::fmt::Write;
write!(plain, "{}", data)?;
plain.push(':'); plain.push(':');
} }
@ -87,7 +89,7 @@ pub fn verify_rsa_ticket(
secret_data: Option<&str>, secret_data: Option<&str>,
min_age: i64, min_age: i64,
max_age: i64, max_age: i64,
) -> Result<(i64, Option<String>), Error> { ) -> Result<(i64, Option<Userid>), Error> {
use std::collections::VecDeque; use std::collections::VecDeque;
@ -145,5 +147,5 @@ pub fn verify_rsa_ticket(
bail!("invalid ticket - timestamp too old."); bail!("invalid ticket - timestamp too old.");
} }
Ok((age, data)) Ok((age, data.map(|s| s.parse()).transpose()?))
} }

View File

@ -54,7 +54,12 @@ fn worker_task_abort() -> Result<(), Error> {
} }
let errmsg = errmsg1.clone(); let errmsg = errmsg1.clone();
let res = server::WorkerTask::new_thread("garbage_collection", None, "root@pam", true, move |worker| { let res = server::WorkerTask::new_thread(
"garbage_collection",
None,
proxmox_backup::api2::types::Userid::root_userid().clone(),
true,
move |worker| {
println!("WORKER {}", worker); println!("WORKER {}", worker);
let result = garbage_collection(&worker); let result = garbage_collection(&worker);
@ -68,7 +73,8 @@ fn worker_task_abort() -> Result<(), Error> {
} }
Ok(()) Ok(())
}); },
);
match res { match res {
Err(err) => { Err(err) => {