2021-01-22 14:53:51 +00:00
//! Access control (Users, Permissions and Authentication)
2020-04-17 12:11:25 +00:00
use anyhow ::{ bail , format_err , Error } ;
2019-01-30 14:14:20 +00:00
2019-11-21 12:10:49 +00:00
use serde_json ::{ json , Value } ;
2020-10-08 08:34:07 +00:00
use std ::collections ::HashMap ;
use std ::collections ::HashSet ;
2019-11-21 12:10:49 +00:00
2020-01-21 11:28:01 +00:00
use proxmox ::api ::router ::{ Router , SubdirMap } ;
2020-11-16 13:37:22 +00:00
use proxmox ::api ::{ api , Permission , RpcEnvironment } ;
2020-01-21 11:28:01 +00:00
use proxmox ::{ http_err , list_subdirs_api_method } ;
2020-11-16 13:37:22 +00:00
use proxmox ::{ identity , sortable } ;
2019-11-21 12:10:49 +00:00
2020-04-09 08:19:38 +00:00
use crate ::api2 ::types ::* ;
2020-11-16 13:37:22 +00:00
use crate ::auth_helpers ::* ;
use crate ::server ::ticket ::ApiTicket ;
use crate ::tools ::ticket ::{ self , Empty , Ticket } ;
2020-04-17 09:04:36 +00:00
2020-10-08 08:34:07 +00:00
use crate ::config ::acl as acl_config ;
2020-11-16 13:37:22 +00:00
use crate ::config ::acl ::{ PRIVILEGES , PRIV_PERMISSIONS_MODIFY , PRIV_SYS_AUDIT } ;
2020-04-16 08:01:59 +00:00
use crate ::config ::cached_user_info ::CachedUserInfo ;
2020-11-16 13:37:22 +00:00
use crate ::config ::tfa ::TfaChallenge ;
2020-04-09 08:19:38 +00:00
2020-04-13 09:09:44 +00:00
pub mod acl ;
2020-11-16 13:37:22 +00:00
pub mod domain ;
2020-04-17 12:03:24 +00:00
pub mod role ;
2020-11-16 13:37:22 +00:00
pub mod tfa ;
pub mod user ;
2021-01-20 16:23:51 +00:00
#[ allow(clippy::large_enum_variant) ]
2020-11-16 13:37:22 +00:00
enum AuthResult {
/// Successful authentication which does not require a new ticket.
Success ,
/// Successful authentication which requires a ticket to be created.
CreateTicket ,
/// A partial ticket which requires a 2nd factor will be created.
Partial ( TfaChallenge ) ,
}
2019-01-30 14:14:20 +00:00
2020-07-21 09:10:37 +00:00
fn authenticate_user (
2020-08-06 13:46:01 +00:00
userid : & Userid ,
2020-07-21 09:10:37 +00:00
password : & str ,
path : Option < String > ,
privs : Option < String > ,
port : Option < u16 > ,
2020-11-16 13:37:22 +00:00
tfa_challenge : Option < String > ,
) -> Result < AuthResult , Error > {
2020-04-16 08:01:59 +00:00
let user_info = CachedUserInfo ::new ( ) ? ;
2020-10-23 11:33:21 +00:00
let auth_id = Authid ::from ( userid . clone ( ) ) ;
if ! user_info . is_active_auth_id ( & auth_id ) {
2020-04-16 08:01:59 +00:00
bail! ( " user account disabled or expired. " ) ;
}
2020-11-16 13:37:22 +00:00
if let Some ( tfa_challenge ) = tfa_challenge {
return authenticate_2nd ( userid , & tfa_challenge , password ) ;
}
2019-03-05 11:53:59 +00:00
if password . starts_with ( " PBS: " ) {
2020-08-12 10:05:52 +00:00
if let Ok ( ticket_userid ) = Ticket ::< Userid > ::parse ( password )
. and_then ( | ticket | ticket . verify ( public_auth_key ( ) , " PBS " , None ) )
{
if * userid = = ticket_userid {
2020-11-16 13:37:22 +00:00
return Ok ( AuthResult ::CreateTicket ) ;
2019-03-05 11:53:59 +00:00
}
2020-08-12 10:05:52 +00:00
bail! ( " ticket login failed - wrong userid " ) ;
2019-03-05 11:53:59 +00:00
}
2020-07-21 09:10:37 +00:00
} else if password . starts_with ( " PBSTERM: " ) {
if path . is_none ( ) | | privs . is_none ( ) | | port . is_none ( ) {
bail! ( " cannot check termnal ticket without path, priv and port " ) ;
}
2020-08-12 10:05:52 +00:00
let path = path . ok_or_else ( | | format_err! ( " missing path for termproxy ticket " ) ) ? ;
2020-11-16 13:37:22 +00:00
let privilege_name =
privs . ok_or_else ( | | format_err! ( " missing privilege name for termproxy ticket " ) ) ? ;
2020-08-12 10:05:52 +00:00
let port = port . ok_or_else ( | | format_err! ( " missing port for termproxy ticket " ) ) ? ;
2020-11-16 13:37:22 +00:00
if let Ok ( Empty ) = Ticket ::parse ( password ) . and_then ( | ticket | {
ticket . verify (
2020-08-12 10:05:52 +00:00
public_auth_key ( ) ,
ticket ::TERM_PREFIX ,
Some ( & ticket ::term_aad ( userid , & path , port ) ) ,
2020-11-16 13:37:22 +00:00
)
} ) {
2020-07-21 09:10:37 +00:00
for ( name , privilege ) in PRIVILEGES {
if * name = = privilege_name {
let mut path_vec = Vec ::new ( ) ;
for part in path . split ( '/' ) {
if part ! = " " {
path_vec . push ( part ) ;
}
}
2020-10-23 11:33:21 +00:00
user_info . check_privs ( & auth_id , & path_vec , * privilege , false ) ? ;
2020-11-16 13:37:22 +00:00
return Ok ( AuthResult ::Success ) ;
2020-07-21 09:10:37 +00:00
}
}
bail! ( " No such privilege " ) ;
}
2019-03-05 11:53:59 +00:00
}
2020-11-16 13:37:22 +00:00
let _ : ( ) = crate ::auth ::authenticate_user ( userid , password ) ? ;
Ok ( match crate ::config ::tfa ::login_challenge ( userid ) ? {
None = > AuthResult ::CreateTicket ,
Some ( challenge ) = > AuthResult ::Partial ( challenge ) ,
} )
}
fn authenticate_2nd (
userid : & Userid ,
challenge_ticket : & str ,
response : & str ,
) -> Result < AuthResult , Error > {
let challenge : TfaChallenge = Ticket ::< ApiTicket > ::parse ( & challenge_ticket ) ?
. verify_with_time_frame ( public_auth_key ( ) , " PBS " , Some ( userid . as_str ( ) ) , - 120 .. 240 ) ?
. require_partial ( ) ? ;
let _ : ( ) = crate ::config ::tfa ::verify_challenge ( userid , & challenge , response . parse ( ) ? ) ? ;
Ok ( AuthResult ::CreateTicket )
2019-01-30 14:14:20 +00:00
}
2019-11-27 13:19:36 +00:00
#[ api(
input : {
properties : {
username : {
2020-08-06 13:46:01 +00:00
type : Userid ,
2019-11-27 13:19:36 +00:00
} ,
password : {
2020-04-09 08:19:38 +00:00
schema : PASSWORD_SCHEMA ,
2019-11-27 13:19:36 +00:00
} ,
2020-07-21 09:10:37 +00:00
path : {
type : String ,
description : " Path for verifying terminal tickets. " ,
optional : true ,
} ,
privs : {
type : String ,
description : " Privilege for verifying terminal tickets. " ,
optional : true ,
} ,
port : {
type : Integer ,
description : " Port for verifying terminal tickets. " ,
optional : true ,
} ,
2020-11-16 13:37:22 +00:00
" tfa-challenge " : {
type : String ,
description : " The signed TFA challenge string the user wants to respond to. " ,
optional : true ,
} ,
2019-11-27 09:05:37 +00:00
} ,
} ,
2019-11-27 13:19:36 +00:00
returns : {
properties : {
username : {
type : String ,
description : " User name. " ,
} ,
ticket : {
type : String ,
description : " Auth ticket. " ,
} ,
CSRFPreventionToken : {
type : String ,
2021-01-08 11:57:14 +00:00
description :
" Cross Site Request Forgery Prevention Token. \
For partial tickets this is the string \ " invalid \" . " ,
2019-11-27 13:19:36 +00:00
} ,
2019-11-27 09:05:37 +00:00
} ,
} ,
2019-11-27 13:19:36 +00:00
protected : true ,
2020-04-16 08:01:59 +00:00
access : {
permission : & Permission ::World ,
} ,
2019-11-27 13:19:36 +00:00
) ]
2019-11-27 09:05:37 +00:00
/// Create or verify authentication ticket.
///
/// Returns: An authentication ticket with additional infos.
2021-01-22 14:53:51 +00:00
pub fn create_ticket (
2020-08-06 13:46:01 +00:00
username : Userid ,
2020-07-21 09:10:37 +00:00
password : String ,
path : Option < String > ,
privs : Option < String > ,
port : Option < u16 > ,
2020-11-16 13:37:22 +00:00
tfa_challenge : Option < String > ,
2020-10-15 15:43:42 +00:00
rpcenv : & mut dyn RpcEnvironment ,
2020-07-21 09:10:37 +00:00
) -> Result < Value , Error > {
2020-11-16 13:37:22 +00:00
match authenticate_user ( & username , & password , path , privs , port , tfa_challenge ) {
Ok ( AuthResult ::Success ) = > Ok ( json! ( { " username " : username } ) ) ,
Ok ( AuthResult ::CreateTicket ) = > {
let api_ticket = ApiTicket ::full ( username . clone ( ) ) ;
let ticket = Ticket ::new ( " PBS " , & api_ticket ) ? . sign ( private_auth_key ( ) , None ) ? ;
2019-11-29 08:51:27 +00:00
let token = assemble_csrf_prevention_token ( csrf_secret ( ) , & username ) ;
2019-01-30 14:14:20 +00:00
2020-11-16 13:37:22 +00:00
crate ::server ::rest ::auth_logger ( ) ?
. log ( format! ( " successful auth for user ' {} ' " , username ) ) ;
2019-01-30 14:14:20 +00:00
2019-10-26 09:36:01 +00:00
Ok ( json! ( {
2019-01-30 14:14:20 +00:00
" username " : username ,
" ticket " : ticket ,
" CSRFPreventionToken " : token ,
2019-10-26 09:36:01 +00:00
} ) )
2019-01-30 14:14:20 +00:00
}
2020-11-16 13:37:22 +00:00
Ok ( AuthResult ::Partial ( challenge ) ) = > {
let api_ticket = ApiTicket ::partial ( challenge ) ;
let ticket = Ticket ::new ( " PBS " , & api_ticket ) ?
. sign ( private_auth_key ( ) , Some ( username . as_str ( ) ) ) ? ;
Ok ( json! ( {
" username " : username ,
" ticket " : ticket ,
2021-01-08 11:57:14 +00:00
" CSRFPreventionToken " : " invalid " ,
2020-11-16 13:37:22 +00:00
} ) )
}
2019-01-30 14:14:20 +00:00
Err ( err ) = > {
2020-10-15 15:43:42 +00:00
let client_ip = match rpcenv . get_client_ip ( ) . map ( | addr | addr . ip ( ) ) {
Some ( ip ) = > format! ( " {} " , ip ) ,
None = > " unknown " . into ( ) ,
} ;
2020-10-16 09:06:48 +00:00
let msg = format! (
" authentication failure; rhost={} user={} msg={} " ,
client_ip ,
username ,
err . to_string ( )
) ;
2020-11-04 15:12:13 +00:00
crate ::server ::rest ::auth_logger ( ) ? . log ( & msg ) ;
2020-10-16 09:06:48 +00:00
log ::error! ( " {} " , msg ) ;
2020-07-29 07:38:11 +00:00
Err ( http_err! ( UNAUTHORIZED , " permission check failed. " ) )
2019-01-30 14:14:20 +00:00
}
}
}
2020-04-09 08:19:38 +00:00
#[ api(
2020-12-05 15:20:29 +00:00
protected : true ,
2020-04-09 08:19:38 +00:00
input : {
properties : {
userid : {
2020-08-06 13:46:01 +00:00
type : Userid ,
2020-04-09 08:19:38 +00:00
} ,
password : {
schema : PASSWORD_SCHEMA ,
} ,
} ,
} ,
2020-04-16 08:01:59 +00:00
access : {
2021-01-13 16:26:15 +00:00
description : " Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm. " ,
2020-04-16 08:01:59 +00:00
permission : & Permission ::Anybody ,
} ,
2020-04-09 08:19:38 +00:00
) ]
/// Change user password
///
/// Each user is allowed to change his own password. Superuser
/// can change all passwords.
2021-01-22 14:53:51 +00:00
pub fn change_password (
2020-08-06 13:46:01 +00:00
userid : Userid ,
2020-04-09 08:19:38 +00:00
password : String ,
rpcenv : & mut dyn RpcEnvironment ,
) -> Result < Value , Error > {
2020-12-30 11:21:13 +00:00
let current_auth : Authid = rpcenv
2020-10-23 11:33:21 +00:00
. get_auth_id ( )
2020-12-30 11:21:13 +00:00
. ok_or_else ( | | format_err! ( " no authid available " ) ) ?
2020-08-06 13:46:01 +00:00
. parse ( ) ? ;
2020-04-09 08:19:38 +00:00
2020-12-30 11:21:13 +00:00
if current_auth . is_token ( ) {
bail! ( " API tokens cannot access this API endpoint " ) ;
}
let current_user = current_auth . user ( ) ;
let mut allowed = userid = = * current_user ;
2020-04-09 08:19:38 +00:00
2020-04-17 09:04:36 +00:00
if ! allowed {
let user_info = CachedUserInfo ::new ( ) ? ;
2020-10-23 11:33:21 +00:00
let privs = user_info . lookup_privs ( & current_auth , & [ ] ) ;
2021-01-13 16:26:15 +00:00
if user_info . is_superuser ( & current_auth ) {
2020-11-16 13:37:22 +00:00
allowed = true ;
}
2021-01-13 16:26:15 +00:00
if ( privs & PRIV_PERMISSIONS_MODIFY ) ! = 0 & & userid . realm ( ) ! = " pam " {
allowed = true ;
}
} ;
2020-04-17 09:04:36 +00:00
2020-04-09 08:19:38 +00:00
if ! allowed {
bail! ( " you are not authorized to change the password. " ) ;
}
2020-08-06 13:46:01 +00:00
let authenticator = crate ::auth ::lookup_authenticator ( userid . realm ( ) ) ? ;
authenticator . store_password ( userid . name ( ) , & password ) ? ;
2020-04-09 08:19:38 +00:00
Ok ( Value ::Null )
}
2020-10-08 08:34:07 +00:00
#[ api(
input : {
properties : {
2020-10-30 14:18:41 +00:00
" auth-id " : {
2020-10-08 08:34:07 +00:00
type : Authid ,
optional : true ,
} ,
path : {
schema : ACL_PATH_SCHEMA ,
optional : true ,
} ,
} ,
} ,
access : {
permission : & Permission ::Anybody ,
description : " Requires Sys.Audit on '/access', limited to own privileges otherwise. " ,
} ,
returns : {
description : " Map of ACL path to Map of privilege to propagate bit " ,
type : Object ,
properties : { } ,
additional_properties : true ,
} ,
) ]
/// List permissions of given or currently authenticated user / API token.
///
/// Optionally limited to specific path.
pub fn list_permissions (
auth_id : Option < Authid > ,
path : Option < String > ,
rpcenv : & dyn RpcEnvironment ,
) -> Result < HashMap < String , HashMap < String , bool > > , Error > {
let current_auth_id : Authid = rpcenv . get_auth_id ( ) . unwrap ( ) . parse ( ) ? ;
let user_info = CachedUserInfo ::new ( ) ? ;
let user_privs = user_info . lookup_privs ( & current_auth_id , & [ " access " ] ) ;
2021-01-20 16:23:51 +00:00
let auth_id = match auth_id {
Some ( auth_id ) if auth_id = = current_auth_id = > current_auth_id ,
Some ( auth_id ) = > {
if user_privs & PRIV_SYS_AUDIT ! = 0
| | ( auth_id . is_token ( )
2020-10-08 08:34:07 +00:00
& & ! current_auth_id . is_token ( )
2021-01-20 16:23:51 +00:00
& & auth_id . user ( ) = = current_auth_id . user ( ) )
{
auth_id
} else {
bail! ( " not allowed to list permissions of {} " , auth_id ) ;
2020-11-16 13:37:22 +00:00
}
2021-01-20 16:23:51 +00:00
} ,
None = > current_auth_id ,
2020-10-08 08:34:07 +00:00
} ;
fn populate_acl_paths (
mut paths : HashSet < String > ,
node : acl_config ::AclTreeNode ,
2020-11-16 13:37:22 +00:00
path : & str ,
2020-10-08 08:34:07 +00:00
) -> HashSet < String > {
for ( sub_path , child_node ) in node . children {
let sub_path = format! ( " {} / {} " , path , & sub_path ) ;
paths = populate_acl_paths ( paths , child_node , & sub_path ) ;
paths . insert ( sub_path ) ;
}
paths
}
let paths = match path {
Some ( path ) = > {
let mut paths = HashSet ::new ( ) ;
paths . insert ( path ) ;
paths
2020-11-16 13:37:22 +00:00
}
2020-10-08 08:34:07 +00:00
None = > {
let mut paths = HashSet ::new ( ) ;
let ( acl_tree , _ ) = acl_config ::config ( ) ? ;
paths = populate_acl_paths ( paths , acl_tree . root , " " ) ;
// default paths, returned even if no ACL exists
paths . insert ( " / " . to_string ( ) ) ;
paths . insert ( " /access " . to_string ( ) ) ;
paths . insert ( " /datastore " . to_string ( ) ) ;
paths . insert ( " /remote " . to_string ( ) ) ;
paths . insert ( " /system " . to_string ( ) ) ;
paths
2020-11-16 13:37:22 +00:00
}
2020-10-08 08:34:07 +00:00
} ;
2020-11-16 13:37:22 +00:00
let map = paths . into_iter ( ) . fold (
HashMap ::new ( ) ,
| mut map : HashMap < String , HashMap < String , bool > > , path : String | {
2020-10-08 08:34:07 +00:00
let split_path = acl_config ::split_acl_path ( path . as_str ( ) ) ;
let ( privs , propagated_privs ) = user_info . lookup_privs_details ( & auth_id , & split_path ) ;
match privs {
0 = > map , // Don't leak ACL paths where we don't have any privileges
_ = > {
2020-11-16 13:37:22 +00:00
let priv_map =
PRIVILEGES
. iter ( )
. fold ( HashMap ::new ( ) , | mut priv_map , ( name , value ) | {
if value & privs ! = 0 {
priv_map
. insert ( name . to_string ( ) , value & propagated_privs ! = 0 ) ;
}
priv_map
} ) ;
2020-10-08 08:34:07 +00:00
map . insert ( path , priv_map ) ;
map
2020-11-16 13:37:22 +00:00
}
}
} ,
) ;
2020-10-08 08:34:07 +00:00
Ok ( map )
}
2019-11-21 12:10:49 +00:00
#[ sortable ]
2020-04-09 11:34:07 +00:00
const SUBDIRS : SubdirMap = & sorted! ( [
2020-04-13 09:09:44 +00:00
( " acl " , & acl ::ROUTER ) ,
2020-11-16 13:37:22 +00:00
( " password " , & Router ::new ( ) . put ( & API_METHOD_CHANGE_PASSWORD ) ) ,
2020-04-09 08:19:38 +00:00
(
2020-11-16 13:37:22 +00:00
" permissions " ,
& Router ::new ( ) . get ( & API_METHOD_LIST_PERMISSIONS )
2020-04-09 08:19:38 +00:00
) ,
2020-11-16 13:37:22 +00:00
( " ticket " , & Router ::new ( ) . post ( & API_METHOD_CREATE_TICKET ) ) ,
2020-04-09 09:36:45 +00:00
( " domains " , & domain ::ROUTER ) ,
2020-04-17 12:03:24 +00:00
( " roles " , & role ::ROUTER ) ,
2020-04-09 08:19:38 +00:00
( " users " , & user ::ROUTER ) ,
2020-11-16 13:37:22 +00:00
( " tfa " , & tfa ::ROUTER ) ,
2020-04-09 11:34:07 +00:00
] ) ;
2019-11-21 08:36:41 +00:00
pub const ROUTER : Router = Router ::new ( )
. get ( & list_subdirs_api_method! ( SUBDIRS ) )
. subdirs ( SUBDIRS ) ;