From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id BD20B62D23 for ; Wed, 28 Oct 2020 12:36:44 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id BB3D11E840 for ; Wed, 28 Oct 2020 12:36:44 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id EEE671E836 for ; Wed, 28 Oct 2020 12:36:43 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id B308845E42 for ; Wed, 28 Oct 2020 12:36:43 +0100 (CET) From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= To: pbs-devel@lists.proxmox.com Date: Wed, 28 Oct 2020 12:36:27 +0100 Message-Id: <20201028113632.814586-7-f.gruenbichler@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201028113632.814586-1-f.gruenbichler@proxmox.com> References: <20201028113632.814586-1-f.gruenbichler@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [rest.rs] Subject: [pbs-devel] [PATCH proxmox-backup 04/16] REST: extract and handle API tokens X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 28 Oct 2020 11:36:44 -0000 and refactor handling of headers in the REST server while we're at it. Signed-off-by: Fabian Grünbichler --- src/server/rest.rs | 109 +++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/src/server/rest.rs b/src/server/rest.rs index 2b835c4a..365e3570 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -531,51 +531,89 @@ async fn handle_static_file_download(filename: PathBuf) -> Result (Option, Option, Option) { +fn extract_lang_header(headers: &http::HeaderMap) -> Option { + if let Some(raw_cookie) = headers.get("COOKIE") { + if let Ok(cookie) = raw_cookie.to_str() { + return tools::extract_cookie(cookie, "PBSLangCookie"); + } + } - let mut ticket = None; - let mut language = None; + None +} + +struct UserAuthData{ + ticket: String, + csrf_token: Option, +} + +enum AuthData { + User(UserAuthData), + ApiToken(String), +} + +fn extract_auth_data(headers: &http::HeaderMap) -> Option { if let Some(raw_cookie) = headers.get("COOKIE") { if let Ok(cookie) = raw_cookie.to_str() { - ticket = tools::extract_cookie(cookie, "PBSAuthCookie"); - language = tools::extract_cookie(cookie, "PBSLangCookie"); + 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, + })); + } } } - let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { - Some(Ok(v)) => Some(v.to_owned()), + match headers.get("AUTHORIZATION").map(|v| v.to_str()) { + Some(Ok(v)) => Some(AuthData::ApiToken(v.to_owned())), _ => None, - }; - - (ticket, csrf_token, language) + } } fn check_auth( method: &hyper::Method, - ticket: &Option, - csrf_token: &Option, + auth_data: &AuthData, user_info: &CachedUserInfo, ) -> Result { - let ticket_lifetime = tools::ticket::TICKET_LIFETIME; + match auth_data { + AuthData::User(user_auth_data) => { + let ticket = user_auth_data.ticket.clone(); + let ticket_lifetime = tools::ticket::TICKET_LIFETIME; - let ticket = ticket.as_ref().map(String::as_str); - let userid: Userid = Ticket::parse(&ticket.ok_or_else(|| format_err!("missing ticket"))?)? - .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?; + let userid: Userid = Ticket::parse(&ticket)? + .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?; - let auth_id = Authid::from(userid.clone()); - if !user_info.is_active_auth_id(&auth_id) { - bail!("user account disabled or expired."); - } + 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) = csrf_token { - verify_csrf_prevention_token(csrf_secret(), &userid, &csrf_token, -300, ticket_lifetime)?; - } else { - bail!("missing CSRF prevention token"); + 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"); + } + } + + 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()?; + + let tokensecret = parts.next() + .ok_or_else(|| format_err!("failed to split API token header"))?; + crate::config::token_shadow::verify_secret(&tokenid, &tokensecret)?; + + Ok(tokenid) } } - - Ok(Authid::from(userid)) } async fn handle_request( @@ -631,8 +669,11 @@ async fn handle_request( } if auth_required { - let (ticket, csrf_token, _) = extract_auth_data(&parts.headers); - match check_auth(&method, &ticket, &csrf_token, &user_info) { + 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 { Ok(authid) => rpcenv.set_auth_id(Some(authid.to_string())), Err(err) => { // always delay unauthorized calls by 3 seconds (from start of request) @@ -685,14 +726,14 @@ async fn handle_request( } if comp_len == 0 { - let (ticket, csrf_token, language) = extract_auth_data(&parts.headers); - if ticket != None { - match check_auth(&method, &ticket, &csrf_token, &user_info) { - Ok(auth_id) => { + 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() => { let userid = auth_id.user(); let new_csrf_token = assemble_csrf_prevention_token(csrf_secret(), userid); return Ok(get_index(Some(userid.clone()), Some(new_csrf_token), language, &api, parts)); - } + }, _ => { tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; return Ok(get_index(None, None, language, &api, parts)); -- 2.20.1