diff --git a/proxmox-rest-server/examples/minimal-rest-server.rs b/proxmox-rest-server/examples/minimal-rest-server.rs index b1ef9335..6400fdbf 100644 --- a/proxmox-rest-server/examples/minimal-rest-server.rs +++ b/proxmox-rest-server/examples/minimal-rest-server.rs @@ -1,20 +1,24 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use anyhow::{bail, format_err, Error}; use lazy_static::lazy_static; +use hyper::{Body, Response, Method}; +use http::request::Parts; +use http::HeaderMap; use proxmox::api::{api, router::SubdirMap, Router, RpcEnvironmentType, UserInformation}; use proxmox::list_subdirs_api_method; -use proxmox_rest_server::{ApiAuth, ApiConfig, AuthError, RestServer, RestEnvironment}; -// Create a Dummy User info and auth system -// Normally this would check and authenticate the user +use proxmox_rest_server::{ServerAdapter, ApiConfig, AuthError, RestServer, RestEnvironment}; + +// Create a Dummy User information system struct DummyUserInfo; impl UserInformation for DummyUserInfo { fn is_superuser(&self, _userid: &str) -> bool { + // Always return true here, so we have access to everthing true } fn is_group_member(&self, _userid: &str, group: &str) -> bool { @@ -25,14 +29,17 @@ impl UserInformation for DummyUserInfo { } } -struct DummyAuth; +struct MinimalServer; -impl ApiAuth for DummyAuth { - fn check_auth<'a>( - &'a self, - _headers: &'a http::HeaderMap, - _method: &'a hyper::Method, - ) -> Pin), AuthError>> + Send + 'a>> { +// implement the server adapter +impl ServerAdapter for MinimalServer { + + // normally this would check and authenticate the user + fn check_auth( + &self, + _headers: &HeaderMap, + _method: &Method, + ) -> Pin), AuthError>> + Send>> { Box::pin(async move { // get some global/cached userinfo let userinfo: Box = Box::new(DummyUserInfo); @@ -40,21 +47,21 @@ impl ApiAuth for DummyAuth { Ok(("User".to_string(), userinfo)) }) } -} -// this should return the index page of the webserver -// iow. what the user browses to - -fn get_index<'a>( - _env: RestEnvironment, - _parts: http::request::Parts, -) -> Pin> + Send + 'a>> { - Box::pin(async move { - // build an index page - http::Response::builder() - .body("hello world".into()) - .unwrap() - }) + // this should return the index page of the webserver + // iow. what the user browses to + fn get_index( + &self, + _env: RestEnvironment, + _parts: Parts, + ) -> Pin> + Send>> { + Box::pin(async move { + // build an index page + http::Response::builder() + .body("hello world".into()) + .unwrap() + }) + } } // a few examples on how to do api calls with the Router @@ -190,8 +197,7 @@ async fn run() -> Result<(), Error> { "/var/tmp/", &ROUTER, RpcEnvironmentType::PUBLIC, - Arc::new(DummyAuth {}), - &get_index, + MinimalServer, )?; let rest_server = RestServer::new(config); diff --git a/proxmox-rest-server/src/api_config.rs b/proxmox-rest-server/src/api_config.rs index c7c71ec0..99990114 100644 --- a/proxmox-rest-server/src/api_config.rs +++ b/proxmox-rest-server/src/api_config.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use std::time::SystemTime; use std::fs::metadata; use std::sync::{Arc, Mutex, RwLock}; -use std::future::Future; use std::pin::Pin; use anyhow::{bail, Error, format_err}; @@ -16,9 +15,8 @@ use serde::Serialize; use proxmox::api::{ApiMethod, Router, RpcEnvironmentType, UserInformation}; use proxmox::tools::fs::{create_path, CreateOptions}; -use crate::{ApiAuth, AuthError, FileLogger, FileLogOptions, CommandSocket, RestEnvironment}; +use crate::{ServerAdapter, AuthError, FileLogger, FileLogOptions, CommandSocket, RestEnvironment}; -pub type GetIndexFn = &'static (dyn Fn(RestEnvironment, Parts) -> Pin> + Send>> + Send + Sync); /// REST server configuration pub struct ApiConfig { @@ -30,8 +28,7 @@ pub struct ApiConfig { template_files: RwLock>, request_log: Option>>, auth_log: Option>>, - api_auth: Arc, - get_index_fn: GetIndexFn, + adapter: Pin>, } impl ApiConfig { @@ -53,8 +50,7 @@ impl ApiConfig { basedir: B, router: &'static Router, env_type: RpcEnvironmentType, - api_auth: Arc, - get_index_fn: GetIndexFn, + adapter: impl ServerAdapter + 'static, ) -> Result { Ok(Self { basedir: basedir.into(), @@ -65,8 +61,7 @@ impl ApiConfig { template_files: RwLock::new(HashMap::new()), request_log: None, auth_log: None, - api_auth, - get_index_fn, + adapter: Box::pin(adapter), }) } @@ -75,7 +70,7 @@ impl ApiConfig { rest_env: RestEnvironment, parts: Parts, ) -> Response { - (self.get_index_fn)(rest_env, parts).await + self.adapter.get_index(rest_env, parts).await } pub(crate) async fn check_auth( @@ -83,7 +78,7 @@ impl ApiConfig { headers: &http::HeaderMap, method: &hyper::Method, ) -> Result<(String, Box), AuthError> { - self.api_auth.check_auth(headers, method).await + self.adapter.check_auth(headers, method).await } pub(crate) fn find_method( diff --git a/proxmox-rest-server/src/lib.rs b/proxmox-rest-server/src/lib.rs index bb29295c..d72936c2 100644 --- a/proxmox-rest-server/src/lib.rs +++ b/proxmox-rest-server/src/lib.rs @@ -21,6 +21,9 @@ use std::pin::Pin; use anyhow::{bail, format_err, Error}; use nix::unistd::Pid; +use hyper::{Body, Response, Method}; +use http::request::Parts; +use http::HeaderMap; use proxmox::tools::fd::Fd; use proxmox::sys::linux::procfs::PidStat; @@ -70,17 +73,26 @@ impl From for AuthError { } } -/// User Authentication trait -pub trait ApiAuth { +/// User Authentication and index/root page generation methods +pub trait ServerAdapter: Send + Sync { + + /// Returns the index/root page + fn get_index( + &self, + rest_env: RestEnvironment, + parts: Parts, + ) -> Pin> + Send>>; + /// Extract user credentials from headers and check them. /// /// If credenthials are valid, returns the username and a /// [UserInformation] object to query additional user data. fn check_auth<'a>( &'a self, - headers: &'a http::HeaderMap, - method: &'a hyper::Method, + headers: &'a HeaderMap, + method: &'a Method, ) -> Pin), AuthError>> + Send + 'a>>; + } lazy_static::lazy_static!{ diff --git a/proxmox-restore-daemon/src/main.rs b/proxmox-restore-daemon/src/main.rs index 1e175abd..6dd25ff5 100644 --- a/proxmox-restore-daemon/src/main.rs +++ b/proxmox-restore-daemon/src/main.rs @@ -7,23 +7,17 @@ use std::os::unix::{ }; use std::path::Path; use std::sync::{Arc, Mutex}; -use std::future::Future; -use std::pin::Pin; use anyhow::{bail, format_err, Error}; use lazy_static::lazy_static; use log::{error, info}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; -use http::request::Parts; -use http::Response; -use hyper::{Body, StatusCode}; -use hyper::header; use proxmox::api::RpcEnvironmentType; use pbs_client::DEFAULT_VSOCK_PORT; -use proxmox_rest_server::{ApiConfig, RestServer, RestEnvironment}; +use proxmox_rest_server::{ApiConfig, RestServer}; mod proxmox_restore_daemon; use proxmox_restore_daemon::*; @@ -93,29 +87,14 @@ fn setup_system_env() -> Result<(), Error> { Ok(()) } -fn get_index<'a>( - _env: RestEnvironment, - _parts: Parts, -) -> Pin> + Send + 'a>> { - Box::pin(async move { - - let index = "

Proxmox Backup Restore Daemon/h1>

"; - - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/html") - .body(index.into()) - .unwrap() - }) -} async fn run() -> Result<(), Error> { watchdog_init(); - let auth_config = Arc::new( - auth::ticket_auth().map_err(|err| format_err!("reading ticket file failed: {}", err))?, - ); - let config = ApiConfig::new("", &ROUTER, RpcEnvironmentType::PUBLIC, auth_config, &get_index)?; + let adaptor = StaticAuthAdapter::new() + .map_err(|err| format_err!("reading ticket file failed: {}", err))?; + + let config = ApiConfig::new("", &ROUTER, RpcEnvironmentType::PUBLIC, adaptor)?; let rest_server = RestServer::new(config); let vsock_fd = get_vsock_fd()?; diff --git a/proxmox-restore-daemon/src/proxmox_restore_daemon/auth.rs b/proxmox-restore-daemon/src/proxmox_restore_daemon/auth.rs index faf57a3d..b57552e1 100644 --- a/proxmox-restore-daemon/src/proxmox_restore_daemon/auth.rs +++ b/proxmox-restore-daemon/src/proxmox_restore_daemon/auth.rs @@ -5,10 +5,13 @@ use std::future::Future; use std::pin::Pin; use anyhow::{bail, format_err, Error}; +use hyper::{Body, Response, Method, StatusCode}; +use http::request::Parts; +use http::HeaderMap; use proxmox::api::UserInformation; -use proxmox_rest_server::{ApiAuth, AuthError}; +use proxmox_rest_server::{ServerAdapter, AuthError, RestEnvironment}; const TICKET_FILE: &str = "/ticket"; @@ -22,15 +25,30 @@ impl UserInformation for SimpleUserInformation { fn lookup_privs(&self, _userid: &str, _path: &[&str]) -> u64 { 0 } } -pub struct StaticAuth { +pub struct StaticAuthAdapter { ticket: String, } -impl ApiAuth for StaticAuth { +impl StaticAuthAdapter { + + pub fn new() -> Result { + let mut ticket_file = File::open(TICKET_FILE)?; + let mut ticket = String::new(); + let len = ticket_file.read_to_string(&mut ticket)?; + if len <= 0 { + bail!("invalid ticket: cannot be empty"); + } + Ok(StaticAuthAdapter { ticket }) + } +} + + +impl ServerAdapter for StaticAuthAdapter { + fn check_auth<'a>( &'a self, - headers: &'a http::HeaderMap, - _method: &'a hyper::Method, + headers: &'a HeaderMap, + _method: &'a Method, ) -> Pin), AuthError>> + Send + 'a>> { Box::pin(async move { @@ -47,14 +65,21 @@ impl ApiAuth for StaticAuth { } }) } -} -pub fn ticket_auth() -> Result { - let mut ticket_file = File::open(TICKET_FILE)?; - let mut ticket = String::new(); - let len = ticket_file.read_to_string(&mut ticket)?; - if len <= 0 { - bail!("invalid ticket: cannot be empty"); + fn get_index( + &self, + _env: RestEnvironment, + _parts: Parts, + ) -> Pin> + Send>> { + Box::pin(async move { + + let index = "

Proxmox Backup Restore Daemon/h1>

"; + + Response::builder() + .status(StatusCode::OK) + .header(hyper::header::CONTENT_TYPE, "text/html") + .body(index.into()) + .unwrap() + }) } - Ok(StaticAuth { ticket }) } diff --git a/proxmox-restore-daemon/src/proxmox_restore_daemon/mod.rs b/proxmox-restore-daemon/src/proxmox_restore_daemon/mod.rs index 58e2bb6e..570f208d 100644 --- a/proxmox-restore-daemon/src/proxmox_restore_daemon/mod.rs +++ b/proxmox-restore-daemon/src/proxmox_restore_daemon/mod.rs @@ -3,6 +3,7 @@ mod api; pub use api::*; pub mod auth; +pub use auth::*; mod watchdog; pub use watchdog::*; diff --git a/src/bin/proxmox-backup-api.rs b/src/bin/proxmox-backup-api.rs index cdfa79f3..a0eef382 100644 --- a/src/bin/proxmox-backup-api.rs +++ b/src/bin/proxmox-backup-api.rs @@ -5,16 +5,17 @@ use anyhow::{bail, Error}; use futures::*; use http::request::Parts; use http::Response; -use hyper::{Body, StatusCode}; -use hyper::header; +use hyper::{Body, Method, StatusCode}; +use http::HeaderMap; use proxmox::try_block; use proxmox::api::RpcEnvironmentType; use proxmox::tools::fs::CreateOptions; +use proxmox::api::UserInformation; -use proxmox_rest_server::{daemon, ApiConfig, RestServer, RestEnvironment}; +use proxmox_rest_server::{daemon, AuthError, ApiConfig, RestServer, RestEnvironment, ServerAdapter}; -use proxmox_backup::server::auth::default_api_auth; +use proxmox_backup::server::auth::check_pbs_auth; use proxmox_backup::auth_helpers::*; use proxmox_backup::config; @@ -27,20 +28,36 @@ fn main() { } } -fn get_index<'a>( - _env: RestEnvironment, - _parts: Parts, -) -> Pin> + Send + 'a>> { - Box::pin(async move { +struct ProxmoxBackupApiAdapter; - let index = "

Proxmox Backup API Server

"; +impl ServerAdapter for ProxmoxBackupApiAdapter { - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/html") - .body(index.into()) - .unwrap() - }) + fn get_index( + &self, + _env: RestEnvironment, + _parts: Parts, + ) -> Pin> + Send>> { + Box::pin(async move { + + let index = "

Proxmox Backup API Server

"; + + Response::builder() + .status(StatusCode::OK) + .header(hyper::header::CONTENT_TYPE, "text/html") + .body(index.into()) + .unwrap() + }) + } + + fn check_auth<'a>( + &'a self, + headers: &'a HeaderMap, + method: &'a Method, + ) -> Pin), AuthError>> + Send + 'a>> { + Box::pin(async move { + check_pbs_auth(headers, method).await + }) + } } async fn run() -> Result<(), Error> { @@ -78,8 +95,7 @@ async fn run() -> Result<(), Error> { pbs_buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED, - default_api_auth(), - &get_index, + ProxmoxBackupApiAdapter, )?; let backup_user = pbs_config::backup_user()?; diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index 57bd50bf..8e4bdcac 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -15,21 +15,23 @@ use url::form_urlencoded; use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype}; use tokio_stream::wrappers::ReceiverStream; use serde_json::{json, Value}; +use http::{Method, HeaderMap}; use proxmox::try_block; -use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; +use proxmox::api::{RpcEnvironment, RpcEnvironmentType, UserInformation}; use proxmox::sys::linux::socket::set_tcp_keepalive; use proxmox::tools::fs::CreateOptions; use pbs_tools::task_log; use pbs_datastore::DataStore; use proxmox_rest_server::{ - rotate_task_log_archive, extract_cookie , ApiConfig, RestServer, RestEnvironment, WorkerTask, + rotate_task_log_archive, extract_cookie , AuthError, ApiConfig, RestServer, RestEnvironment, + ServerAdapter, WorkerTask, }; use proxmox_backup::{ server::{ - auth::default_api_auth, + auth::check_pbs_auth, jobstate::{ self, Job, @@ -81,6 +83,29 @@ fn main() -> Result<(), Error> { } +struct ProxmoxBackupProxyAdapter; + +impl ServerAdapter for ProxmoxBackupProxyAdapter { + + fn get_index( + &self, + env: RestEnvironment, + parts: Parts, + ) -> Pin> + Send>> { + Box::pin(get_index_future(env, parts)) + } + + fn check_auth<'a>( + &'a self, + headers: &'a HeaderMap, + method: &'a Method, + ) -> Pin), AuthError>> + Send + 'a>> { + Box::pin(async move { + check_pbs_auth(headers, method).await + }) + } +} + fn extract_lang_header(headers: &http::HeaderMap) -> Option { if let Some(Ok(cookie)) = headers.get("COOKIE").map(|v| v.to_str()) { return extract_cookie(cookie, "PBSLangCookie"); @@ -88,13 +113,6 @@ fn extract_lang_header(headers: &http::HeaderMap) -> Option { None } -fn get_index<'a>( - env: RestEnvironment, - parts: Parts, -) -> Pin> + Send + 'a>> { - Box::pin(get_index_future(env, parts)) -} - async fn get_index_future( env: RestEnvironment, parts: Parts, @@ -191,8 +209,7 @@ async fn run() -> Result<(), Error> { pbs_buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PUBLIC, - default_api_auth(), - &get_index, + ProxmoxBackupProxyAdapter, )?; config.add_alias("novnc", "/usr/share/novnc-pve"); diff --git a/src/server/auth.rs b/src/server/auth.rs index d6c4a66f..2e6beac7 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -1,9 +1,5 @@ //! Provides authentication primitives for the HTTP server -use std::sync::Arc; -use std::future::Future; -use std::pin::Pin; - use anyhow::format_err; use proxmox::api::UserInformation; @@ -11,7 +7,7 @@ use proxmox::api::UserInformation; use pbs_tools::ticket::{self, Ticket}; use pbs_config::{token_shadow, CachedUserInfo}; use pbs_api_types::{Authid, Userid}; -use proxmox_rest_server::{ApiAuth, AuthError, extract_cookie}; +use proxmox_rest_server::{AuthError, extract_cookie}; use crate::auth_helpers::*; @@ -28,111 +24,93 @@ enum AuthData { ApiToken(String), } -pub struct UserApiAuth {} -pub fn default_api_auth() -> Arc { - Arc::new(UserApiAuth {}) -} - -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) = 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 })); - } +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) = 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())) + 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 async fn check_pbs_auth( + headers: &http::HeaderMap, + method: &hyper::Method, +) -> Result<(String, Box), AuthError> { + + // fixme: make all IO async + + let user_info = CachedUserInfo::new()?; + + let auth_data = extract_auth_data(headers); + match auth_data { + Some(AuthData::User(user_auth_data)) => { + let ticket = user_auth_data.ticket.clone(); + let ticket_lifetime = 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 { - None + return Err(format_err!("missing CSRF prevention token").into()); } } - _ => None, + + Ok((auth_id.to_string(), Box::new(user_info))) } - } + 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()?; - async fn check_auth_async( - &self, - headers: &http::HeaderMap, - method: &hyper::Method, - ) -> Result<(String, Box), AuthError> { - - // fixme: make all IO async - - let user_info = CachedUserInfo::new()?; - - 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 = 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.to_string(), Box::new(user_info))) + if !user_info.is_active_auth_id(&tokenid) { + return Err(format_err!("user account or token disabled or expired.").into()); } - 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"))?; - 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"))?; + token_shadow::verify_secret(&tokenid, &tokensecret)?; - token_shadow::verify_secret(&tokenid, &tokensecret)?; - - Ok((tokenid.to_string(), Box::new(user_info))) - } - None => Err(AuthError::NoData), + Ok((tokenid.to_string(), Box::new(user_info))) } - } -} - -impl ApiAuth for UserApiAuth { - fn check_auth<'a>( - &'a self, - headers: &'a http::HeaderMap, - method: &'a hyper::Method, - ) -> Pin), AuthError>> + Send + 'a>> { - Box::pin(self.check_auth_async(headers, method)) + None => Err(AuthError::NoData), } }