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