2020-05-21 08:14:26 +00:00
use anyhow ::{ bail , Error } ;
use serde_json ::Value ;
use ::serde ::{ Deserialize , Serialize } ;
2020-10-30 11:36:42 +00:00
use proxmox ::api ::{ api , Permission , Router , RpcEnvironment } ;
2020-08-04 09:33:02 +00:00
use proxmox ::tools ::fs ::open_file_locked ;
2020-05-21 08:14:26 +00:00
use crate ::api2 ::types ::* ;
2020-10-30 11:36:42 +00:00
use crate ::config ::acl ::{
PRIV_DATASTORE_AUDIT ,
PRIV_DATASTORE_BACKUP ,
PRIV_DATASTORE_MODIFY ,
PRIV_DATASTORE_PRUNE ,
PRIV_REMOTE_AUDIT ,
PRIV_REMOTE_READ ,
} ;
use crate ::config ::cached_user_info ::CachedUserInfo ;
2020-05-21 08:29:25 +00:00
use crate ::config ::sync ::{ self , SyncJobConfig } ;
2020-05-21 08:14:26 +00:00
2020-10-30 11:36:42 +00:00
pub fn check_sync_job_read_access (
user_info : & CachedUserInfo ,
auth_id : & Authid ,
job : & SyncJobConfig ,
) -> bool {
let datastore_privs = user_info . lookup_privs ( & auth_id , & [ " datastore " , & job . store ] ) ;
if datastore_privs & PRIV_DATASTORE_AUDIT = = 0 {
return false ;
}
let remote_privs = user_info . lookup_privs ( & auth_id , & [ " remote " , & job . remote ] ) ;
remote_privs & PRIV_REMOTE_AUDIT ! = 0
}
2020-11-02 10:48:10 +00:00
2020-10-30 11:36:42 +00:00
// user can run the corresponding pull job
pub fn check_sync_job_modify_access (
user_info : & CachedUserInfo ,
auth_id : & Authid ,
job : & SyncJobConfig ,
) -> bool {
let datastore_privs = user_info . lookup_privs ( & auth_id , & [ " datastore " , & job . store ] ) ;
if datastore_privs & PRIV_DATASTORE_BACKUP = = 0 {
return false ;
}
if let Some ( true ) = job . remove_vanished {
if datastore_privs & PRIV_DATASTORE_PRUNE = = 0 {
return false ;
}
}
let correct_owner = match job . owner {
Some ( ref owner ) = > {
owner = = auth_id
| | ( owner . is_token ( )
& & ! auth_id . is_token ( )
& & owner . user ( ) = = auth_id . user ( ) )
} ,
// default sync owner
2020-11-05 13:03:15 +00:00
None = > auth_id = = Authid ::root_auth_id ( ) ,
2020-10-30 11:36:42 +00:00
} ;
// same permission as changing ownership after syncing
if ! correct_owner & & datastore_privs & PRIV_DATASTORE_MODIFY = = 0 {
return false ;
}
let remote_privs = user_info . lookup_privs ( & auth_id , & [ " remote " , & job . remote , & job . remote_store ] ) ;
remote_privs & PRIV_REMOTE_READ ! = 0
}
2020-05-21 08:14:26 +00:00
#[ api(
input : {
properties : { } ,
} ,
returns : {
description : " List configured jobs. " ,
type : Array ,
2020-05-21 08:29:25 +00:00
items : { type : sync ::SyncJobConfig } ,
2020-05-21 08:14:26 +00:00
} ,
2020-10-30 11:36:42 +00:00
access : {
description : " Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote. " ,
permission : & Permission ::Anybody ,
} ,
2020-05-21 08:14:26 +00:00
) ]
2020-05-21 08:29:25 +00:00
/// List all sync jobs
pub fn list_sync_jobs (
2020-05-21 08:14:26 +00:00
_param : Value ,
mut rpcenv : & mut dyn RpcEnvironment ,
2020-05-21 08:29:25 +00:00
) -> Result < Vec < SyncJobConfig > , Error > {
2020-10-30 11:36:42 +00:00
let auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
let ( config , digest ) = sync ::config ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
let list = config . convert_to_typed_array ( " sync " ) ? ;
2020-05-21 08:14:26 +00:00
rpcenv [ " digest " ] = proxmox ::tools ::digest_to_hex ( & digest ) . into ( ) ;
2020-10-30 11:36:42 +00:00
let list = list
. into_iter ( )
. filter ( | sync_job | check_sync_job_read_access ( & user_info , & auth_id , & sync_job ) )
. collect ( ) ;
Ok ( list )
2020-05-21 08:14:26 +00:00
}
#[ api(
protected : true ,
input : {
properties : {
id : {
schema : JOB_ID_SCHEMA ,
} ,
store : {
schema : DATASTORE_SCHEMA ,
} ,
2020-10-30 11:36:41 +00:00
owner : {
type : Authid ,
optional : true ,
} ,
2020-05-21 08:14:26 +00:00
remote : {
schema : REMOTE_ID_SCHEMA ,
} ,
" remote-store " : {
schema : DATASTORE_SCHEMA ,
} ,
" remove-vanished " : {
schema : REMOVE_VANISHED_BACKUPS_SCHEMA ,
optional : true ,
} ,
comment : {
optional : true ,
schema : SINGLE_LINE_COMMENT_SCHEMA ,
} ,
schedule : {
optional : true ,
2020-05-29 08:53:34 +00:00
schema : SYNC_SCHEDULE_SCHEMA ,
2020-05-21 08:14:26 +00:00
} ,
} ,
} ,
2020-10-30 11:36:42 +00:00
access : {
description : " User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify " ,
permission : & Permission ::Anybody ,
} ,
2020-05-21 08:14:26 +00:00
) ]
2020-05-21 08:29:25 +00:00
/// Create a new sync job.
2020-10-30 11:36:42 +00:00
pub fn create_sync_job (
param : Value ,
rpcenv : & mut dyn RpcEnvironment ,
) -> Result < ( ) , Error > {
let auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-09-28 08:50:44 +00:00
let _lock = open_file_locked ( sync ::SYNC_CFG_LOCKFILE , std ::time ::Duration ::new ( 10 , 0 ) , true ) ? ;
2020-05-21 08:14:26 +00:00
2021-01-15 13:38:27 +00:00
let sync_job : sync ::SyncJobConfig = serde_json ::from_value ( param ) ? ;
2020-10-30 11:36:42 +00:00
if ! check_sync_job_modify_access ( & user_info , & auth_id , & sync_job ) {
bail! ( " permission check failed " ) ;
}
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
let ( mut config , _digest ) = sync ::config ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
if let Some ( _ ) = config . sections . get ( & sync_job . id ) {
bail! ( " job '{}' already exists. " , sync_job . id ) ;
2020-05-21 08:14:26 +00:00
}
2020-05-21 08:29:25 +00:00
config . set_data ( & sync_job . id , " sync " , & sync_job ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
sync ::save_config ( & config ) ? ;
2020-05-21 08:14:26 +00:00
2020-10-28 06:33:05 +00:00
crate ::server ::jobstate ::create_state_file ( " syncjob " , & sync_job . id ) ? ;
2020-08-13 12:30:19 +00:00
2020-05-21 08:14:26 +00:00
Ok ( ( ) )
}
#[ api(
input : {
properties : {
id : {
schema : JOB_ID_SCHEMA ,
} ,
} ,
} ,
2020-12-09 09:54:38 +00:00
returns : { type : sync ::SyncJobConfig } ,
2020-10-30 11:36:42 +00:00
access : {
description : " Limited to sync job entries where user has Datastore.Audit on target datastore, and Remote.Audit on source remote. " ,
permission : & Permission ::Anybody ,
} ,
2020-05-21 08:14:26 +00:00
) ]
2020-05-21 08:29:25 +00:00
/// Read a sync job configuration.
pub fn read_sync_job (
2020-05-21 08:14:26 +00:00
id : String ,
mut rpcenv : & mut dyn RpcEnvironment ,
2020-05-21 08:29:25 +00:00
) -> Result < SyncJobConfig , Error > {
2020-10-30 11:36:42 +00:00
let auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
2020-05-21 08:29:25 +00:00
let ( config , digest ) = sync ::config ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
let sync_job = config . lookup ( " sync " , & id ) ? ;
2020-10-30 11:36:42 +00:00
if ! check_sync_job_read_access ( & user_info , & auth_id , & sync_job ) {
bail! ( " permission check failed " ) ;
}
2020-05-21 08:14:26 +00:00
rpcenv [ " digest " ] = proxmox ::tools ::digest_to_hex ( & digest ) . into ( ) ;
2020-05-21 08:29:25 +00:00
Ok ( sync_job )
2020-05-21 08:14:26 +00:00
}
#[ api() ]
#[ derive(Serialize, Deserialize) ]
#[ serde(rename_all= " kebab-case " ) ]
#[ allow(non_camel_case_types) ]
/// Deletable property name
pub enum DeletableProperty {
2020-10-30 11:36:41 +00:00
/// Delete the owner property.
owner ,
2020-05-21 08:14:26 +00:00
/// Delete the comment property.
comment ,
/// Delete the job schedule.
schedule ,
/// Delete the remove-vanished flag.
remove_vanished ,
}
#[ api(
protected : true ,
input : {
properties : {
id : {
schema : JOB_ID_SCHEMA ,
} ,
store : {
schema : DATASTORE_SCHEMA ,
optional : true ,
} ,
2020-10-30 11:36:41 +00:00
owner : {
type : Authid ,
optional : true ,
} ,
2020-05-21 08:14:26 +00:00
remote : {
schema : REMOTE_ID_SCHEMA ,
optional : true ,
} ,
" remote-store " : {
schema : DATASTORE_SCHEMA ,
optional : true ,
} ,
" remove-vanished " : {
schema : REMOVE_VANISHED_BACKUPS_SCHEMA ,
optional : true ,
} ,
comment : {
optional : true ,
schema : SINGLE_LINE_COMMENT_SCHEMA ,
} ,
schedule : {
optional : true ,
2020-05-29 08:53:34 +00:00
schema : SYNC_SCHEDULE_SCHEMA ,
2020-05-21 08:14:26 +00:00
} ,
delete : {
description : " List of properties to delete. " ,
type : Array ,
optional : true ,
items : {
type : DeletableProperty ,
}
} ,
digest : {
optional : true ,
schema : PROXMOX_CONFIG_DIGEST_SCHEMA ,
} ,
} ,
} ,
2020-10-30 11:36:42 +00:00
access : {
permission : & Permission ::Anybody ,
description : " User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify " ,
} ,
2020-05-21 08:14:26 +00:00
) ]
2020-05-21 08:29:25 +00:00
/// Update sync job config.
pub fn update_sync_job (
2020-05-21 08:14:26 +00:00
id : String ,
store : Option < String > ,
2020-10-30 11:36:41 +00:00
owner : Option < Authid > ,
2020-05-21 08:14:26 +00:00
remote : Option < String > ,
remote_store : Option < String > ,
remove_vanished : Option < bool > ,
comment : Option < String > ,
schedule : Option < String > ,
delete : Option < Vec < DeletableProperty > > ,
digest : Option < String > ,
2020-10-30 11:36:42 +00:00
rpcenv : & mut dyn RpcEnvironment ,
2020-05-21 08:14:26 +00:00
) -> Result < ( ) , Error > {
2020-10-30 11:36:42 +00:00
let auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-09-28 08:50:44 +00:00
let _lock = open_file_locked ( sync ::SYNC_CFG_LOCKFILE , std ::time ::Duration ::new ( 10 , 0 ) , true ) ? ;
2020-05-21 08:14:26 +00:00
// pass/compare digest
2020-05-21 08:29:25 +00:00
let ( mut config , expected_digest ) = sync ::config ( ) ? ;
2020-05-21 08:14:26 +00:00
if let Some ( ref digest ) = digest {
let digest = proxmox ::tools ::hex_to_digest ( digest ) ? ;
crate ::tools ::detect_modified_configuration_file ( & digest , & expected_digest ) ? ;
}
2020-05-21 08:29:25 +00:00
let mut data : sync ::SyncJobConfig = config . lookup ( " sync " , & id ) ? ;
2020-05-21 08:14:26 +00:00
if let Some ( delete ) = delete {
for delete_prop in delete {
match delete_prop {
2020-10-30 11:36:41 +00:00
DeletableProperty ::owner = > { data . owner = None ; } ,
2020-05-21 08:14:26 +00:00
DeletableProperty ::comment = > { data . comment = None ; } ,
DeletableProperty ::schedule = > { data . schedule = None ; } ,
DeletableProperty ::remove_vanished = > { data . remove_vanished = None ; } ,
}
}
}
if let Some ( comment ) = comment {
let comment = comment . trim ( ) . to_string ( ) ;
if comment . is_empty ( ) {
data . comment = None ;
} else {
data . comment = Some ( comment ) ;
}
}
if let Some ( store ) = store { data . store = store ; }
if let Some ( remote ) = remote { data . remote = remote ; }
if let Some ( remote_store ) = remote_store { data . remote_store = remote_store ; }
2020-10-30 11:36:42 +00:00
if let Some ( owner ) = owner { data . owner = Some ( owner ) ; }
2020-10-30 11:36:41 +00:00
2020-05-21 08:14:26 +00:00
if schedule . is_some ( ) { data . schedule = schedule ; }
if remove_vanished . is_some ( ) { data . remove_vanished = remove_vanished ; }
2020-10-30 11:36:42 +00:00
if ! check_sync_job_modify_access ( & user_info , & auth_id , & data ) {
bail! ( " permission check failed " ) ;
}
2020-05-21 08:29:25 +00:00
config . set_data ( & id , " sync " , & data ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
sync ::save_config ( & config ) ? ;
2020-05-21 08:14:26 +00:00
Ok ( ( ) )
}
#[ api(
protected : true ,
input : {
properties : {
id : {
schema : JOB_ID_SCHEMA ,
} ,
digest : {
optional : true ,
schema : PROXMOX_CONFIG_DIGEST_SCHEMA ,
} ,
} ,
} ,
2020-10-30 11:36:42 +00:00
access : {
permission : & Permission ::Anybody ,
description : " User needs Datastore.Backup on target datastore, and Remote.Read on source remote. Additionally, remove_vanished requires Datastore.Prune, and any owner other than the user themselves requires Datastore.Modify " ,
} ,
2020-05-21 08:14:26 +00:00
) ]
2020-05-21 08:29:25 +00:00
/// Remove a sync job configuration
2020-10-30 11:36:42 +00:00
pub fn delete_sync_job (
id : String ,
digest : Option < String > ,
rpcenv : & mut dyn RpcEnvironment ,
) -> Result < ( ) , Error > {
let auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
2020-05-21 08:14:26 +00:00
2020-09-28 08:50:44 +00:00
let _lock = open_file_locked ( sync ::SYNC_CFG_LOCKFILE , std ::time ::Duration ::new ( 10 , 0 ) , true ) ? ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
let ( mut config , expected_digest ) = sync ::config ( ) ? ;
2020-05-21 08:14:26 +00:00
if let Some ( ref digest ) = digest {
let digest = proxmox ::tools ::hex_to_digest ( digest ) ? ;
crate ::tools ::detect_modified_configuration_file ( & digest , & expected_digest ) ? ;
}
2020-10-30 11:36:42 +00:00
match config . lookup ( " sync " , & id ) {
Ok ( job ) = > {
if ! check_sync_job_modify_access ( & user_info , & auth_id , & job ) {
bail! ( " permission check failed " ) ;
}
config . sections . remove ( & id ) ;
} ,
Err ( _ ) = > { bail! ( " job '{}' does not exist. " , id ) } ,
} ;
2020-05-21 08:14:26 +00:00
2020-05-21 08:29:25 +00:00
sync ::save_config ( & config ) ? ;
2020-05-21 08:14:26 +00:00
2020-10-28 06:33:05 +00:00
crate ::server ::jobstate ::remove_state_file ( " syncjob " , & id ) ? ;
2020-08-13 08:29:16 +00:00
2020-05-21 08:14:26 +00:00
Ok ( ( ) )
}
const ITEM_ROUTER : Router = Router ::new ( )
2020-05-21 08:29:25 +00:00
. get ( & API_METHOD_READ_SYNC_JOB )
. put ( & API_METHOD_UPDATE_SYNC_JOB )
. delete ( & API_METHOD_DELETE_SYNC_JOB ) ;
2020-05-21 08:14:26 +00:00
pub const ROUTER : Router = Router ::new ( )
2020-05-21 08:29:25 +00:00
. get ( & API_METHOD_LIST_SYNC_JOBS )
. post ( & API_METHOD_CREATE_SYNC_JOB )
2020-05-29 08:53:35 +00:00
. match_all ( " id " , & ITEM_ROUTER ) ;
2020-11-02 10:48:10 +00:00
#[ test ]
fn sync_job_access_test ( ) -> Result < ( ) , Error > {
let ( user_cfg , _ ) = crate ::config ::user ::test_cfg_from_str ( r ###"
user : noperm @ pbs
user : read @ pbs
user : write @ pbs
" ###).expect( " test user . cfg is not parsable " );
let acl_tree = crate ::config ::acl ::AclTree ::from_raw ( r ###"
acl :1 :/ datastore / localstore1 :read @ pbs , write @ pbs :DatastoreAudit
acl :1 :/ datastore / localstore1 :write @ pbs :DatastoreBackup
acl :1 :/ datastore / localstore2 :write @ pbs :DatastorePowerUser
acl :1 :/ datastore / localstore3 :write @ pbs :DatastoreAdmin
acl :1 :/ remote / remote1 :read @ pbs , write @ pbs :RemoteAudit
acl :1 :/ remote / remote1 / remotestore1 :write @ pbs :RemoteSyncOperator
" ###).expect( " test acl . cfg is not parsable " );
let user_info = CachedUserInfo ::test_new ( user_cfg , acl_tree ) ;
let root_auth_id = Authid ::root_auth_id ( ) ;
let no_perm_auth_id : Authid = " noperm@pbs " . parse ( ) ? ;
let read_auth_id : Authid = " read@pbs " . parse ( ) ? ;
let write_auth_id : Authid = " write@pbs " . parse ( ) ? ;
let mut job = SyncJobConfig {
id : " regular " . to_string ( ) ,
remote : " remote0 " . to_string ( ) ,
remote_store : " remotestore1 " . to_string ( ) ,
store : " localstore0 " . to_string ( ) ,
owner : Some ( write_auth_id . clone ( ) ) ,
comment : None ,
remove_vanished : None ,
schedule : None ,
} ;
// should work without ACLs
assert_eq! ( check_sync_job_read_access ( & user_info , & root_auth_id , & job ) , true ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & root_auth_id , & job ) , true ) ;
// user without permissions must fail
assert_eq! ( check_sync_job_read_access ( & user_info , & no_perm_auth_id , & job ) , false ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & no_perm_auth_id , & job ) , false ) ;
// reading without proper read permissions on either remote or local must fail
assert_eq! ( check_sync_job_read_access ( & user_info , & read_auth_id , & job ) , false ) ;
// reading without proper read permissions on local end must fail
job . remote = " remote1 " . to_string ( ) ;
assert_eq! ( check_sync_job_read_access ( & user_info , & read_auth_id , & job ) , false ) ;
// reading without proper read permissions on remote end must fail
job . remote = " remote0 " . to_string ( ) ;
job . store = " localstore1 " . to_string ( ) ;
assert_eq! ( check_sync_job_read_access ( & user_info , & read_auth_id , & job ) , false ) ;
// writing without proper write permissions on either end must fail
job . store = " localstore0 " . to_string ( ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , false ) ;
// writing without proper write permissions on local end must fail
job . remote = " remote1 " . to_string ( ) ;
// writing without proper write permissions on remote end must fail
job . remote = " remote0 " . to_string ( ) ;
job . store = " localstore1 " . to_string ( ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , false ) ;
// reset remote to one where users have access
job . remote = " remote1 " . to_string ( ) ;
// user with read permission can only read, but not modify/run
assert_eq! ( check_sync_job_read_access ( & user_info , & read_auth_id , & job ) , true ) ;
job . owner = Some ( read_auth_id . clone ( ) ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & read_auth_id , & job ) , false ) ;
job . owner = None ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & read_auth_id , & job ) , false ) ;
job . owner = Some ( write_auth_id . clone ( ) ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & read_auth_id , & job ) , false ) ;
// user with simple write permission can modify/run
assert_eq! ( check_sync_job_read_access ( & user_info , & write_auth_id , & job ) , true ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , true ) ;
// but can't modify/run with deletion
job . remove_vanished = Some ( true ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , false ) ;
// unless they have Datastore.Prune as well
job . store = " localstore2 " . to_string ( ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , true ) ;
// changing owner is not possible
job . owner = Some ( read_auth_id . clone ( ) ) ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , false ) ;
2020-11-05 13:03:15 +00:00
// also not to the default 'root@pam'
2020-11-02 10:48:10 +00:00
job . owner = None ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , false ) ;
// unless they have Datastore.Modify as well
job . store = " localstore3 " . to_string ( ) ;
2021-01-15 13:38:27 +00:00
job . owner = Some ( read_auth_id ) ;
2020-11-02 10:48:10 +00:00
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , true ) ;
job . owner = None ;
assert_eq! ( check_sync_job_modify_access ( & user_info , & write_auth_id , & job ) , true ) ;
Ok ( ( ) )
}