//! Access control (Users, Permissions and Authentication) use anyhow::{bail, format_err, Error}; use serde_json::{json, Value}; use std::collections::HashMap; use std::collections::HashSet; use proxmox::api::router::{Router, SubdirMap}; use proxmox::api::{api, Permission, RpcEnvironment}; use proxmox::{http_err, list_subdirs_api_method}; use proxmox::{identity, sortable}; use crate::api2::types::*; use crate::auth_helpers::*; use crate::server::ticket::ApiTicket; use crate::tools::ticket::{self, Empty, Ticket}; use crate::config::acl as acl_config; use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT}; use crate::config::cached_user_info::CachedUserInfo; use crate::config::tfa::TfaChallenge; pub mod acl; pub mod domain; pub mod role; pub mod tfa; pub mod user; #[allow(clippy::large_enum_variant)] 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), } fn authenticate_user( userid: &Userid, password: &str, path: Option, privs: Option, port: Option, tfa_challenge: Option, ) -> Result { let user_info = CachedUserInfo::new()?; let auth_id = Authid::from(userid.clone()); if !user_info.is_active_auth_id(&auth_id) { bail!("user account disabled or expired."); } if let Some(tfa_challenge) = tfa_challenge { return authenticate_2nd(userid, &tfa_challenge, password); } if password.starts_with("PBS:") { if let Ok(ticket_userid) = Ticket::::parse(password) .and_then(|ticket| ticket.verify(public_auth_key(), "PBS", None)) { if *userid == ticket_userid { return Ok(AuthResult::CreateTicket); } bail!("ticket login failed - wrong userid"); } } 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"); } let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?; let privilege_name = privs.ok_or_else(|| format_err!("missing privilege name for termproxy ticket"))?; let port = port.ok_or_else(|| format_err!("missing port for termproxy ticket"))?; if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| { ticket.verify( public_auth_key(), ticket::TERM_PREFIX, Some(&ticket::term_aad(userid, &path, port)), ) }) { 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); } } user_info.check_privs(&auth_id, &path_vec, *privilege, false)?; return Ok(AuthResult::Success); } } bail!("No such privilege"); } } 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 { let challenge: TfaChallenge = Ticket::::parse(&challenge_ticket)? .verify_with_time_frame(public_auth_key(), "PBS", Some(userid.as_str()), -60..600)? .require_partial()?; let _: () = crate::config::tfa::verify_challenge(userid, &challenge, response.parse()?)?; Ok(AuthResult::CreateTicket) } #[api( input: { properties: { username: { type: Userid, }, password: { schema: PASSWORD_SCHEMA, }, 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, }, "tfa-challenge": { type: String, description: "The signed TFA challenge string the user wants to respond to.", optional: true, }, }, }, returns: { properties: { username: { type: String, description: "User name.", }, ticket: { type: String, description: "Auth ticket.", }, CSRFPreventionToken: { type: String, description: "Cross Site Request Forgery Prevention Token. \ For partial tickets this is the string \"invalid\".", }, }, }, protected: true, access: { permission: &Permission::World, }, )] /// Create or verify authentication ticket. /// /// Returns: An authentication ticket with additional infos. pub fn create_ticket( username: Userid, password: String, path: Option, privs: Option, port: Option, tfa_challenge: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result { 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)?; let token = assemble_csrf_prevention_token(csrf_secret(), &username); crate::server::rest::auth_logger()? .log(format!("successful auth for user '{}'", username)); Ok(json!({ "username": username, "ticket": ticket, "CSRFPreventionToken": token, })) } 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, "CSRFPreventionToken": "invalid", })) } Err(err) => { let client_ip = match rpcenv.get_client_ip().map(|addr| addr.ip()) { Some(ip) => format!("{}", ip), None => "unknown".into(), }; let msg = format!( "authentication failure; rhost={} user={} msg={}", client_ip, username, err.to_string() ); crate::server::rest::auth_logger()?.log(&msg); log::error!("{}", msg); Err(http_err!(UNAUTHORIZED, "permission check failed.")) } } } #[api( protected: true, input: { properties: { userid: { type: Userid, }, password: { schema: PASSWORD_SCHEMA, }, }, }, access: { description: "Everybody is allowed to change their own password. In addition, users with 'Permissions:Modify' privilege may change any password on @pbs realm.", permission: &Permission::Anybody, }, )] /// Change user password /// /// Each user is allowed to change his own password. Superuser /// can change all passwords. pub fn change_password( userid: Userid, password: String, rpcenv: &mut dyn RpcEnvironment, ) -> Result { let current_auth: Authid = rpcenv .get_auth_id() .ok_or_else(|| format_err!("no authid available"))? .parse()?; 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; if !allowed { let user_info = CachedUserInfo::new()?; let privs = user_info.lookup_privs(¤t_auth, &[]); if user_info.is_superuser(¤t_auth) { allowed = true; } if (privs & PRIV_PERMISSIONS_MODIFY) != 0 && userid.realm() != "pam" { allowed = true; } }; if !allowed { bail!("you are not authorized to change the password."); } let authenticator = crate::auth::lookup_authenticator(userid.realm())?; authenticator.store_password(userid.name(), &password)?; Ok(Value::Null) } #[api( input: { properties: { "auth-id": { 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, path: Option, rpcenv: &dyn RpcEnvironment, ) -> Result>, Error> { let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let user_info = CachedUserInfo::new()?; let user_privs = user_info.lookup_privs(¤t_auth_id, &["access"]); 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() && !current_auth_id.is_token() && auth_id.user() == current_auth_id.user()) { auth_id } else { bail!("not allowed to list permissions of {}", auth_id); } }, None => current_auth_id, }; fn populate_acl_paths( mut paths: HashSet, node: acl_config::AclTreeNode, path: &str, ) -> HashSet { 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 } 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 } }; let map = paths.into_iter().fold( HashMap::new(), |mut map: HashMap>, path: String| { 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 _ => { 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 }); map.insert(path, priv_map); map } } }, ); Ok(map) } #[sortable] const SUBDIRS: SubdirMap = &sorted!([ ("acl", &acl::ROUTER), ("password", &Router::new().put(&API_METHOD_CHANGE_PASSWORD)), ( "permissions", &Router::new().get(&API_METHOD_LIST_PERMISSIONS) ), ("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)), ("domains", &domain::ROUTER), ("roles", &role::ROUTER), ("users", &user::ROUTER), ("tfa", &tfa::ROUTER), ]); pub const ROUTER: Router = Router::new() .get(&list_subdirs_api_method!(SUBDIRS)) .subdirs(SUBDIRS);