diff --git a/src/bin/proxmox-backup-api.rs b/src/bin/proxmox-backup-api.rs index 7d800259..e514a801 100644 --- a/src/bin/proxmox-backup-api.rs +++ b/src/bin/proxmox-backup-api.rs @@ -6,8 +6,11 @@ use proxmox::api::RpcEnvironmentType; //use proxmox_backup::tools; //use proxmox_backup::api_schema::config::*; -use proxmox_backup::server::rest::*; -use proxmox_backup::server; +use proxmox_backup::server::{ + self, + auth::default_api_auth, + rest::*, +}; use proxmox_backup::tools::daemon; use proxmox_backup::auth_helpers::*; use proxmox_backup::config; @@ -53,7 +56,11 @@ async fn run() -> Result<(), Error> { let _ = csrf_secret(); // load with lazy_static let mut config = server::ApiConfig::new( - buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED)?; + buildcfg::JS_DIR, + &proxmox_backup::api2::ROUTER, + RpcEnvironmentType::PRIVILEGED, + default_api_auth(), + )?; let mut commando_sock = server::CommandoSocket::new(server::our_ctrl_sock()); diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index 541d34b5..7e026455 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -14,6 +14,7 @@ use proxmox::api::RpcEnvironmentType; use proxmox_backup::{ backup::DataStore, server::{ + auth::default_api_auth, WorkerTask, ApiConfig, rest::*, @@ -84,7 +85,11 @@ async fn run() -> Result<(), Error> { let _ = csrf_secret(); // load with lazy_static let mut config = ApiConfig::new( - buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PUBLIC)?; + buildcfg::JS_DIR, + &proxmox_backup::api2::ROUTER, + RpcEnvironmentType::PUBLIC, + default_api_auth(), + )?; // Enable experimental tape UI if tape.cfg exists if Path::new("/etc/proxmox-backup/tape.cfg").exists() { diff --git a/src/server/auth.rs b/src/server/auth.rs index e3c1f408..0a9a740c 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -1,101 +1,140 @@ //! Provides authentication primitives for the HTTP server -use anyhow::{bail, format_err, Error}; +use anyhow::{format_err, Error}; + +use std::sync::Arc; -use crate::tools::ticket::Ticket; -use crate::auth_helpers::*; -use crate::tools; -use crate::config::cached_user_info::CachedUserInfo; use crate::api2::types::{Authid, Userid}; +use crate::auth_helpers::*; +use crate::config::cached_user_info::CachedUserInfo; +use crate::tools; +use crate::tools::ticket::Ticket; use hyper::header; use percent_encoding::percent_decode_str; -pub struct UserAuthData { +pub enum AuthError { + Generic(Error), + NoData, +} + +impl From for AuthError { + fn from(err: Error) -> Self { + AuthError::Generic(err) + } +} + +pub trait ApiAuth { + fn check_auth( + &self, + headers: &http::HeaderMap, + method: &hyper::Method, + user_info: &CachedUserInfo, + ) -> Result; +} + +struct UserAuthData { ticket: String, csrf_token: Option, } -pub enum AuthData { +enum AuthData { User(UserAuthData), ApiToken(String), } -pub fn extract_auth_data(headers: &http::HeaderMap) -> Option { - if let Some(raw_cookie) = headers.get(header::COOKIE) { - if let Ok(cookie) = raw_cookie.to_str() { - if let Some(ticket) = tools::extract_cookie(cookie, "PBSAuthCookie") { - let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { - Some(Ok(v)) => Some(v.to_owned()), - _ => None, - }; - return Some(AuthData::User(UserAuthData { - ticket, - csrf_token, - })); - } - } - } - - match headers.get(header::AUTHORIZATION).map(|v| v.to_str()) { - Some(Ok(v)) => { - if v.starts_with("PBSAPIToken ") || v.starts_with("PBSAPIToken=") { - Some(AuthData::ApiToken(v["PBSAPIToken ".len()..].to_owned())) - } else { - None - } - }, - _ => None, - } +pub struct UserApiAuth {} +pub fn default_api_auth() -> Arc { + Arc::new(UserApiAuth {}) } -pub fn check_auth( - method: &hyper::Method, - auth_data: &AuthData, - user_info: &CachedUserInfo, -) -> Result { - match auth_data { - AuthData::User(user_auth_data) => { - let ticket = user_auth_data.ticket.clone(); - let ticket_lifetime = tools::ticket::TICKET_LIFETIME; - - let userid: Userid = Ticket::::parse(&ticket)? - .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)? - .require_full()?; - - let auth_id = Authid::from(userid.clone()); - if !user_info.is_active_auth_id(&auth_id) { - bail!("user account disabled or expired."); - } - - if method != hyper::Method::GET { - if let Some(csrf_token) = &user_auth_data.csrf_token { - verify_csrf_prevention_token(csrf_secret(), &userid, &csrf_token, -300, ticket_lifetime)?; - } else { - bail!("missing CSRF prevention token"); +impl UserApiAuth { + fn extract_auth_data(headers: &http::HeaderMap) -> Option { + if let Some(raw_cookie) = headers.get(header::COOKIE) { + if let Ok(cookie) = raw_cookie.to_str() { + if let Some(ticket) = tools::extract_cookie(cookie, "PBSAuthCookie") { + let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { + Some(Ok(v)) => Some(v.to_owned()), + _ => None, + }; + return Some(AuthData::User(UserAuthData { ticket, csrf_token })); } } + } - Ok(auth_id) - }, - AuthData::ApiToken(api_token) => { - let mut parts = api_token.splitn(2, ':'); - let tokenid = parts.next() - .ok_or_else(|| format_err!("failed to split API token header"))?; - let tokenid: Authid = tokenid.parse()?; - - if !user_info.is_active_auth_id(&tokenid) { - bail!("user account or token disabled or expired."); + match headers.get(header::AUTHORIZATION).map(|v| v.to_str()) { + Some(Ok(v)) => { + if v.starts_with("PBSAPIToken ") || v.starts_with("PBSAPIToken=") { + Some(AuthData::ApiToken(v["PBSAPIToken ".len()..].to_owned())) + } else { + None + } } - - let tokensecret = parts.next() - .ok_or_else(|| format_err!("failed to split API token header"))?; - let tokensecret = percent_decode_str(tokensecret) - .decode_utf8() - .map_err(|_| format_err!("failed to decode API token header"))?; - - crate::config::token_shadow::verify_secret(&tokenid, &tokensecret)?; - - Ok(tokenid) + _ => None, + } + } +} + +impl ApiAuth for UserApiAuth { + fn check_auth( + &self, + headers: &http::HeaderMap, + method: &hyper::Method, + user_info: &CachedUserInfo, + ) -> Result { + let auth_data = Self::extract_auth_data(headers); + match auth_data { + Some(AuthData::User(user_auth_data)) => { + let ticket = user_auth_data.ticket.clone(); + let ticket_lifetime = tools::ticket::TICKET_LIFETIME; + + let userid: Userid = Ticket::::parse(&ticket)? + .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)? + .require_full()?; + + let auth_id = Authid::from(userid.clone()); + if !user_info.is_active_auth_id(&auth_id) { + return Err(format_err!("user account disabled or expired.").into()); + } + + if method != hyper::Method::GET { + if let Some(csrf_token) = &user_auth_data.csrf_token { + verify_csrf_prevention_token( + csrf_secret(), + &userid, + &csrf_token, + -300, + ticket_lifetime, + )?; + } else { + return Err(format_err!("missing CSRF prevention token").into()); + } + } + + Ok(auth_id) + } + Some(AuthData::ApiToken(api_token)) => { + let mut parts = api_token.splitn(2, ':'); + let tokenid = parts + .next() + .ok_or_else(|| format_err!("failed to split API token header"))?; + let tokenid: Authid = tokenid.parse()?; + + if !user_info.is_active_auth_id(&tokenid) { + return Err(format_err!("user account or token disabled or expired.").into()); + } + + let tokensecret = parts + .next() + .ok_or_else(|| format_err!("failed to split API token header"))?; + let tokensecret = percent_decode_str(tokensecret) + .decode_utf8() + .map_err(|_| format_err!("failed to decode API token header"))?; + + crate::config::token_shadow::verify_secret(&tokenid, &tokensecret)?; + + Ok(tokenid) + } + None => Err(AuthError::NoData), } } } diff --git a/src/server/config.rs b/src/server/config.rs index 9094fa80..ad378b0a 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -13,6 +13,7 @@ use proxmox::api::{ApiMethod, Router, RpcEnvironmentType}; use proxmox::tools::fs::{create_path, CreateOptions}; use crate::tools::{FileLogger, FileLogOptions}; +use super::auth::ApiAuth; pub struct ApiConfig { basedir: PathBuf, @@ -23,11 +24,16 @@ pub struct ApiConfig { template_files: RwLock>, request_log: Option>>, pub enable_tape_ui: bool, + pub api_auth: Arc, } impl ApiConfig { - - pub fn new>(basedir: B, router: &'static Router, env_type: RpcEnvironmentType) -> Result { + pub fn new>( + basedir: B, + router: &'static Router, + env_type: RpcEnvironmentType, + api_auth: Arc, + ) -> Result { Ok(Self { basedir: basedir.into(), router, @@ -37,7 +43,8 @@ impl ApiConfig { template_files: RwLock::new(HashMap::new()), request_log: None, enable_tape_ui: false, - }) + api_auth, + }) } pub fn find_method( diff --git a/src/server/rest.rs b/src/server/rest.rs index 13d379c7..c482bab2 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -30,10 +30,10 @@ use proxmox::api::{ }; use proxmox::http_err; +use super::auth::AuthError; use super::environment::RestEnvironment; use super::formatter::*; use super::ApiConfig; -use super::auth::{check_auth, extract_auth_data}; use crate::api2::types::{Authid, Userid}; use crate::auth_helpers::*; @@ -678,6 +678,7 @@ async fn handle_request( rpcenv.set_client_ip(Some(*peer)); let user_info = CachedUserInfo::new()?; + let auth = &api.api_auth; let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500); @@ -703,13 +704,15 @@ async fn handle_request( } if auth_required { - let auth_result = match extract_auth_data(&parts.headers) { - Some(auth_data) => check_auth(&method, &auth_data, &user_info), - None => Err(format_err!("no authentication credentials provided.")), - }; - match auth_result { + match auth.check_auth(&parts.headers, &method, &user_info) { Ok(authid) => rpcenv.set_auth_id(Some(authid.to_string())), - Err(err) => { + Err(auth_err) => { + let err = match auth_err { + AuthError::Generic(err) => err, + AuthError::NoData => { + format_err!("no authentication credentials provided.") + } + }; let peer = peer.ip(); auth_logger()?.log(format!( "authentication failure; rhost={} msg={}", @@ -772,9 +775,9 @@ async fn handle_request( if comp_len == 0 { let language = extract_lang_header(&parts.headers); - if let Some(auth_data) = extract_auth_data(&parts.headers) { - match check_auth(&method, &auth_data, &user_info) { - Ok(auth_id) if !auth_id.is_token() => { + match auth.check_auth(&parts.headers, &method, &user_info) { + Ok(auth_id) => { + if !auth_id.is_token() { let userid = auth_id.user(); let new_csrf_token = assemble_csrf_prevention_token(csrf_secret(), userid); return Ok(get_index( @@ -785,14 +788,13 @@ async fn handle_request( parts, )); } - _ => { - tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await; - return Ok(get_index(None, None, language, &api, parts)); - } } - } else { - return Ok(get_index(None, None, language, &api, parts)); + Err(AuthError::Generic(_)) => { + tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await; + } + Err(AuthError::NoData) => {} } + return Ok(get_index(None, None, language, &api, parts)); } else { let filename = api.find_alias(&components); let compression = extract_compression_method(&parts.headers);