mirror of
https://git.proxmox.com/git/proxmox
synced 2025-06-03 15:13:30 +00:00
new proxmox-login package
Author: Wofgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
12674a37e0
commit
26f586d5eb
25
proxmox-login/Cargo.toml
Normal file
25
proxmox-login/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "proxmox-login"
|
||||
version = "0.1.0"
|
||||
description = "proxmox product authentication api"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
exclude.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
serde = { workspace = true, features = [ "derive" ] }
|
||||
serde_json.workspace = true
|
||||
|
||||
# For webauthn types
|
||||
webauthn-rs = { workspace = true, optional = true }
|
||||
|
||||
# For `Authentication::set_auth_headers`
|
||||
http = { version = "0.2.4", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
webauthn = [ "dep:webauthn-rs" ]
|
||||
http = ["dep:http"]
|
6
proxmox-login/debian/changelog
Normal file
6
proxmox-login/debian/changelog
Normal file
@ -0,0 +1,6 @@
|
||||
rust-proxmox-login (0.1.0-1) unstable; urgency=medium
|
||||
|
||||
* initial Debian package
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 04 May 2023 08:40:38 +0200
|
||||
|
74
proxmox-login/debian/control
Normal file
74
proxmox-login/debian/control
Normal file
@ -0,0 +1,74 @@
|
||||
Source: rust-proxmox-login
|
||||
Section: rust
|
||||
Priority: optional
|
||||
Build-Depends: debhelper (>= 12),
|
||||
dh-cargo (>= 25),
|
||||
cargo:native <!nocheck>,
|
||||
rustc:native <!nocheck>,
|
||||
libstd-rust-dev <!nocheck>,
|
||||
librust-base64-0.13+default-dev <!nocheck>,
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~) <!nocheck>,
|
||||
librust-serde-1+default-dev <!nocheck>,
|
||||
librust-serde-1+derive-dev <!nocheck>,
|
||||
librust-serde-json-1+default-dev <!nocheck>
|
||||
Maintainer: Proxmox Support Team <support@proxmox.com>
|
||||
Standards-Version: 4.6.1
|
||||
Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/proxmox-login]
|
||||
Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/proxmox-login
|
||||
X-Cargo-Crate: proxmox-login
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: librust-proxmox-login-dev
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
librust-base64-0.13+default-dev,
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-serde-1+default-dev,
|
||||
librust-serde-1+derive-dev,
|
||||
librust-serde-json-1+default-dev
|
||||
Suggests:
|
||||
librust-proxmox-login+http-dev (= ${binary:Version}),
|
||||
librust-proxmox-login+webauthn-dev (= ${binary:Version})
|
||||
Provides:
|
||||
librust-proxmox-login+default-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0+default-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1+default-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1.0-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1.0+default-dev (= ${binary:Version})
|
||||
Description: Proxmox product authentication api - Rust source code
|
||||
This package contains the source for the Rust proxmox-login crate, packaged by
|
||||
debcargo for use with cargo and dh-cargo.
|
||||
|
||||
Package: librust-proxmox-login+http-dev
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
librust-proxmox-login-dev (= ${binary:Version}),
|
||||
librust-http-0.2+default-dev (>= 0.2.4-~~)
|
||||
Provides:
|
||||
librust-proxmox-login-0+http-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1+http-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1.0+http-dev (= ${binary:Version})
|
||||
Description: Proxmox product authentication api - feature "http"
|
||||
This metapackage enables feature "http" for the Rust proxmox-login crate, by
|
||||
pulling in any additional dependencies needed by that feature.
|
||||
|
||||
Package: librust-proxmox-login+webauthn-dev
|
||||
Architecture: any
|
||||
Multi-Arch: same
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
librust-proxmox-login-dev (= ${binary:Version}),
|
||||
librust-webauthn-rs-0.3+default-dev
|
||||
Provides:
|
||||
librust-proxmox-login-0+webauthn-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1+webauthn-dev (= ${binary:Version}),
|
||||
librust-proxmox-login-0.1.0+webauthn-dev (= ${binary:Version})
|
||||
Description: Proxmox product authentication api - feature "webauthn"
|
||||
This metapackage enables feature "webauthn" for the Rust proxmox-login crate,
|
||||
by pulling in any additional dependencies needed by that feature.
|
16
proxmox-login/debian/copyright
Normal file
16
proxmox-login/debian/copyright
Normal file
@ -0,0 +1,16 @@
|
||||
Copyright (C) 2023 Proxmox Server Solutions GmbH
|
||||
|
||||
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
7
proxmox-login/debian/debcargo.toml
Normal file
7
proxmox-login/debian/debcargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
overlay = "."
|
||||
crate_src_path = ".."
|
||||
maintainer = "Proxmox Support Team <support@proxmox.com>"
|
||||
|
||||
[source]
|
||||
#vcs_git = "git://git.proxmox.com/git/proxmox.git"
|
||||
#vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
|
71
proxmox-login/src/api.rs
Normal file
71
proxmox-login/src/api.rs
Normal file
@ -0,0 +1,71 @@
|
||||
//! API types used during authentication.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The JSON parameter object for the `/api2/access/ticket` API call.
|
||||
///
|
||||
/// Note that for Proxmox VE up to including version 7 the `new_format` parameter has to be used,
|
||||
/// if TFA should be supported, as this crate does not support the old TFA login mechanism.
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct CreateTicket {
|
||||
/// With webauthn the format of half-authenticated tickts changed. New
|
||||
/// clients should pass 1 here and not worry about the old format. The old
|
||||
/// format is deprecated and will be retired with PVE-8.0
|
||||
#[serde(deserialize_with = "crate::parse::deserialize_bool")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "new-format")]
|
||||
pub new_format: Option<bool>,
|
||||
|
||||
/// One-time password for Two-factor authentication.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub otp: Option<String>,
|
||||
|
||||
/// The secret password. This can also be a valid ticket.
|
||||
pub password: String,
|
||||
|
||||
/// Verify ticket, and check if user have access 'privs' on 'path'
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
|
||||
/// Verify ticket, and check if user have access 'privs' on 'path'
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub privs: Option<String>,
|
||||
|
||||
/// You can optionally pass the realm using this parameter. Normally the
|
||||
/// realm is simply added to the username <username>@<relam>.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub realm: Option<String>,
|
||||
|
||||
/// The signed TFA challenge string the user wants to respond to.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "tfa-challenge")]
|
||||
pub tfa_challenge: Option<String>,
|
||||
|
||||
/// User name
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// The API response for a *complete* (both factors) `api2/access/ticket` call.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct CreateTicketResponse {
|
||||
/// The CSRF prevention token.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "CSRFPreventionToken")]
|
||||
pub csrfprevention_token: Option<String>,
|
||||
|
||||
/// The cluster's visual name.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub clustername: Option<String>,
|
||||
|
||||
/// The ticket as is supposed to be used in the authentication header.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ticket: Option<String>,
|
||||
|
||||
/// The full userid with the `@realm` part.
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub data: Option<T>,
|
||||
}
|
85
proxmox-login/src/error.rs
Normal file
85
proxmox-login/src/error.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Error types.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Ticket parsing error.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TicketError;
|
||||
|
||||
impl std::error::Error for TicketError {}
|
||||
|
||||
impl fmt::Display for TicketError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("invalid ticket")
|
||||
}
|
||||
}
|
||||
|
||||
/// Error parsing an API response.
|
||||
#[derive(Debug)]
|
||||
pub enum ResponseError {
|
||||
/// An error happened when decoding the JSON response.
|
||||
Json(serde_json::Error),
|
||||
|
||||
/// Some unexpected error occurred.
|
||||
Msg(&'static str),
|
||||
|
||||
/// Failed to parse the ticket contained in the response.
|
||||
Ticket(TicketError),
|
||||
}
|
||||
|
||||
impl std::error::Error for ResponseError {}
|
||||
|
||||
impl fmt::Display for ResponseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ResponseError::Json(err) => write!(f, "bad ticket response: {err}"),
|
||||
ResponseError::Msg(err) => write!(f, "bad ticket response: {err}"),
|
||||
ResponseError::Ticket(err) => write!(f, "failed to parse ticket in response: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ResponseError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ResponseError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ResponseError {
|
||||
fn from(err: &'static str) -> Self {
|
||||
ResponseError::Msg(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TicketError> for ResponseError {
|
||||
fn from(err: TicketError) -> Self {
|
||||
ResponseError::Ticket(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error creating a request for Two-Factor-Authentication.
|
||||
#[derive(Debug)]
|
||||
pub enum TfaError {
|
||||
/// The chosen TFA method is not available.
|
||||
Unavailable,
|
||||
|
||||
/// A serialization error occurred.
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl std::error::Error for TfaError {}
|
||||
|
||||
impl fmt::Display for TfaError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
TfaError::Unavailable => f.write_str("the chosen TFA method is not available"),
|
||||
TfaError::Json(err) => write!(f, "a serialization error occurred: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for TfaError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
TfaError::Json(err)
|
||||
}
|
||||
}
|
292
proxmox-login/src/lib.rs
Normal file
292
proxmox-login/src/lib.rs
Normal file
@ -0,0 +1,292 @@
|
||||
//! This package provides helpers for logging into the APIs of Proxmox products such as Proxmox VE
|
||||
//! or Proxmox Backup.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod parse;
|
||||
|
||||
pub mod api;
|
||||
pub mod error;
|
||||
pub mod tfa;
|
||||
pub mod ticket;
|
||||
|
||||
const CONTENT_TYPE_JSON: &str = "application/json";
|
||||
|
||||
#[doc(inline)]
|
||||
pub use ticket::{Authentication, Ticket};
|
||||
|
||||
use error::{ResponseError, TfaError, TicketError};
|
||||
|
||||
/// The header name for the CSRF prevention token.
|
||||
pub const CSRF_HEADER_NAME: &str = "CSRFPreventionToken";
|
||||
|
||||
/// A request to be sent to the ticket API call.
|
||||
///
|
||||
/// Note that the body is always JSON (`application/json`) and request method is POST.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Request {
|
||||
pub url: String,
|
||||
|
||||
/// This is always `application/json`.
|
||||
pub content_type: &'static str,
|
||||
|
||||
/// The `Content-length` header field.
|
||||
pub content_length: usize,
|
||||
|
||||
/// The body.
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// Login or ticket renewal request builder.
|
||||
///
|
||||
/// This takes an API URL and either a valid ticket or a userid (name + real) and password in order
|
||||
/// to create an HTTP [`Request`] to renew or create a new API ticket.
|
||||
///
|
||||
/// Note that for Proxmox VE versions up to including 7, a compatibility flag is required to
|
||||
/// support Two-Factor-Authentication.
|
||||
#[derive(Debug)]
|
||||
pub struct Login {
|
||||
api_url: String,
|
||||
userid: String,
|
||||
password: String,
|
||||
pve_compat: bool,
|
||||
}
|
||||
|
||||
fn normalize_url(mut api_url: String) -> String {
|
||||
api_url.truncate(api_url.trim_end_matches('/').len());
|
||||
api_url
|
||||
}
|
||||
|
||||
impl Login {
|
||||
/// Prepare a request given an existing ticket string.
|
||||
pub fn renew(api_url: impl Into<String>, ticket: impl Into<String>) -> Result<Self, TicketError> {
|
||||
Ok(Self::renew_ticket(api_url, ticket.into().parse()?))
|
||||
}
|
||||
|
||||
/// Switch to a different url on the same server.
|
||||
pub fn set_url(&mut self, api_url: impl Into<String>) {
|
||||
self.api_url = api_url.into();
|
||||
}
|
||||
|
||||
/// Prepare a request given an already parsed ticket.
|
||||
pub fn renew_ticket(api_url: impl Into<String>, ticket: Ticket) -> Self {
|
||||
Self {
|
||||
api_url: normalize_url(api_url.into()),
|
||||
pve_compat: ticket.product() == "PVE",
|
||||
userid: ticket.userid().to_string(),
|
||||
password: ticket.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare a request given a userid and password.
|
||||
pub fn new(api_url: impl Into<String>, userid: impl Into<String>, password: impl Into<String>) -> Self {
|
||||
Self {
|
||||
api_url: normalize_url(api_url.into()),
|
||||
userid: userid.into(),
|
||||
password: password.into(),
|
||||
pve_compat: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Proxmox VE compatibility parameter for Two-Factor-Authentication support.
|
||||
pub fn pve_compatibility(mut self, compatibility: bool) -> Self {
|
||||
self.pve_compat = compatibility;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an HTTP [`Request`] from the current data.
|
||||
///
|
||||
/// If the request returns a successful result, the response's body should be passed to the
|
||||
/// [`response`](Login::response) method in order to extract the validated ticket or
|
||||
/// Two-Factor-Authentication challenge.
|
||||
pub fn request(&self) -> Request {
|
||||
let request = api::CreateTicket {
|
||||
new_format: self.pve_compat.then_some(true),
|
||||
username: self.userid.clone(),
|
||||
password: self.password.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&request)
|
||||
.unwrap(); // this can never fail
|
||||
|
||||
Request {
|
||||
url: format!("{}/api2/json/access/ticket", self.api_url),
|
||||
content_type: CONTENT_TYPE_JSON,
|
||||
content_length: body.len(),
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the result body of a [`CreateTicket`](api::CreateTicket) API request.
|
||||
///
|
||||
/// On success, this will either yield an [`Authentication`] or a [`SecondFactorChallenge`] if
|
||||
/// Two-Factor-Authentication is required.
|
||||
pub fn response(&self, body: &str) -> Result<TicketResult, ResponseError> {
|
||||
use ticket::TicketResponse;
|
||||
|
||||
let response: api::ApiResponse<api::CreateTicketResponse> = serde_json::from_str(body)?;
|
||||
let response = response.data.ok_or("missing response data")?;
|
||||
|
||||
if response.username != self.userid {
|
||||
return Err("ticket response contained unexpected userid".into());
|
||||
}
|
||||
|
||||
let ticket: TicketResponse = match response.ticket {
|
||||
Some(ticket) => ticket.parse()?,
|
||||
None => return Err("missing ticket".into()),
|
||||
};
|
||||
|
||||
Ok(match ticket {
|
||||
TicketResponse::Full(ticket) => {
|
||||
if ticket.userid() != self.userid {
|
||||
return Err("returned ticket contained unexpected userid".into());
|
||||
}
|
||||
TicketResult::Full(Authentication {
|
||||
csrfprevention_token: response
|
||||
.csrfprevention_token
|
||||
.ok_or("missing CSRFPreventionToken in ticket response")?,
|
||||
clustername: response.clustername,
|
||||
api_url: self.api_url.clone(),
|
||||
userid: response.username,
|
||||
ticket,
|
||||
})
|
||||
}
|
||||
|
||||
TicketResponse::Tfa(ticket, challenge) => {
|
||||
TicketResult::TfaRequired(SecondFactorChallenge {
|
||||
api_url: self.api_url.clone(),
|
||||
pve_compat: self.pve_compat,
|
||||
userid: response.username,
|
||||
ticket,
|
||||
challenge,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the result of a ticket call. It will either yield a final ticket, or a TFA challenge.
|
||||
///
|
||||
/// This is serializable in order to easily store it for later reuse.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum TicketResult {
|
||||
/// The response contained a valid ticket.
|
||||
Full(Authentication),
|
||||
|
||||
/// The response returned a Two-Factor-Authentication challenge.
|
||||
TfaRequired(SecondFactorChallenge),
|
||||
}
|
||||
|
||||
/// A ticket call can returned a TFA challenge. The user should inspect the
|
||||
/// [`challenge`](tfa::TfaChallenge) member and call one of the `respond_*` methods which will
|
||||
/// yield a HTTP [`Request`] which should be used to finish the authentication.
|
||||
///
|
||||
/// Finally, the response should be passed to the [`response`](SecondFactorChallenge::response)
|
||||
/// method to get the ticket.
|
||||
///
|
||||
/// This is serializable in order to easily store it for later reuse.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct SecondFactorChallenge {
|
||||
api_url: String,
|
||||
pve_compat: bool,
|
||||
userid: String,
|
||||
ticket: String,
|
||||
pub challenge: tfa::TfaChallenge,
|
||||
}
|
||||
|
||||
impl SecondFactorChallenge {
|
||||
/// Create a HTTP request responding to a Yubico OTP challenge.
|
||||
///
|
||||
/// Errors with `TfaError::Unavailable` if Yubic OTP is not available.
|
||||
pub fn respond_yubico(&self, code: &str) -> Result<Request, TfaError> {
|
||||
if !self.challenge.yubico {
|
||||
Err(TfaError::Unavailable)
|
||||
} else {
|
||||
Ok(self.respond_raw(&format!("yubico:{code}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a HTTP request responding with a TOTP value.
|
||||
///
|
||||
/// Errors with `TfaError::Unavailable` if TOTP is not available.
|
||||
pub fn respond_totp(&self, code: &str) -> Result<Request, TfaError> {
|
||||
if !self.challenge.totp {
|
||||
Err(TfaError::Unavailable)
|
||||
} else {
|
||||
Ok(self.respond_raw(&format!("totp:{code}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a HTTP request responding with a recovery code.
|
||||
///
|
||||
/// Errors with `TfaError::Unavailable` if no recovery codes are available.
|
||||
pub fn respond_recovery(&self, code: &str) -> Result<Request, TfaError> {
|
||||
if !self.challenge.recovery.is_available() {
|
||||
Err(TfaError::Unavailable)
|
||||
} else {
|
||||
Ok(self.respond_raw(&format!("recovery:{code}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webauthn")]
|
||||
/// Create a HTTP request responding with a FIDO2/webauthn result JSON string.
|
||||
///
|
||||
/// Errors with `TfaError::Unavailable` if no webauthn challenge was available.
|
||||
pub fn respond_webauthn(&self, json_string: &str) -> Result<Request, TfaError> {
|
||||
if self.challenge.webauthn.is_none() {
|
||||
Err(TfaError::Unavailable)
|
||||
} else {
|
||||
Ok(self.respond_raw(&format!("webauthn:{json_string}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a HTTP request using a raw response.
|
||||
///
|
||||
/// A raw response is the response string prefixed with its challenge type and a colon.
|
||||
pub fn respond_raw(&self, data: &str) -> Request {
|
||||
let request = api::CreateTicket {
|
||||
new_format: self.pve_compat.then_some(true),
|
||||
username: self.userid.clone(),
|
||||
password: data.to_string(),
|
||||
tfa_challenge: Some(self.ticket.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&request).unwrap();
|
||||
|
||||
Request {
|
||||
url: format!("{}/api2/json/access/ticket", self.api_url),
|
||||
content_type: CONTENT_TYPE_JSON,
|
||||
content_length: body.len(),
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
/// Deal with the API's response object to extract the ticket.
|
||||
pub fn response(&self, body: &[u8]) -> Result<Authentication, ResponseError> {
|
||||
let response: api::ApiResponse<api::CreateTicketResponse> = serde_json::from_slice(body)?;
|
||||
let response = response.data.ok_or("missing response data")?;
|
||||
|
||||
if response.username != self.userid {
|
||||
return Err("ticket response contained unexpected userid".into());
|
||||
}
|
||||
|
||||
let ticket: Ticket = response.ticket.ok_or("no ticket in response")?.parse()?;
|
||||
|
||||
if ticket.userid() != self.userid {
|
||||
return Err("returned ticket contained unexpected userid".into());
|
||||
}
|
||||
|
||||
Ok(Authentication {
|
||||
ticket,
|
||||
csrfprevention_token: response
|
||||
.csrfprevention_token
|
||||
.ok_or("missing CSRFPreventionToken in ticket response")?,
|
||||
clustername: response.clustername,
|
||||
userid: response.username,
|
||||
api_url: self.api_url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
239
proxmox-login/src/parse.rs
Normal file
239
proxmox-login/src/parse.rs
Normal file
@ -0,0 +1,239 @@
|
||||
//! Some parsing helpers for the PVE API, mainly to deal with perl's untypedness.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::Unexpected;
|
||||
|
||||
// Boolean:
|
||||
|
||||
pub trait FromBool: Sized + Default {
|
||||
fn from_bool(value: bool) -> Self;
|
||||
}
|
||||
|
||||
impl FromBool for bool {
|
||||
fn from_bool(value: bool) -> Self {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
impl FromBool for Option<bool> {
|
||||
fn from_bool(value: bool) -> Self {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_bool<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: FromBool,
|
||||
{
|
||||
deserializer.deserialize_any(BoolVisitor::<T>::new())
|
||||
}
|
||||
|
||||
struct BoolVisitor<T>(std::marker::PhantomData<T>);
|
||||
|
||||
impl<T> BoolVisitor<T> {
|
||||
fn new() -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: FromBool> serde::de::DeserializeSeed<'de> for BoolVisitor<T> {
|
||||
type Value = T;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserialize_bool(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> serde::de::Visitor<'de> for BoolVisitor<T>
|
||||
where
|
||||
T: FromBool,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a boolean-ish...")
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(self)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
fn visit_bool<E: serde::de::Error>(self, value: bool) -> Result<Self::Value, E> {
|
||||
Ok(Self::Value::from_bool(value))
|
||||
}
|
||||
|
||||
fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
|
||||
Ok(Self::Value::from_bool(value != 0))
|
||||
}
|
||||
|
||||
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
|
||||
Ok(Self::Value::from_bool(value != 0))
|
||||
}
|
||||
|
||||
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
|
||||
Ok(Self::Value::from_bool(value != 0))
|
||||
}
|
||||
|
||||
fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
|
||||
Ok(Self::Value::from_bool(value != 0))
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
let value = if value.eq_ignore_ascii_case("true")
|
||||
|| value.eq_ignore_ascii_case("yes")
|
||||
|| value.eq_ignore_ascii_case("on")
|
||||
{
|
||||
true
|
||||
} else if value.eq_ignore_ascii_case("false")
|
||||
|| value.eq_ignore_ascii_case("no")
|
||||
|| value.eq_ignore_ascii_case("off")
|
||||
{
|
||||
false
|
||||
} else {
|
||||
return Err(E::invalid_value(
|
||||
serde::de::Unexpected::Str(value),
|
||||
&"a boolean-like value",
|
||||
));
|
||||
};
|
||||
Ok(Self::Value::from_bool(value))
|
||||
}
|
||||
}
|
||||
|
||||
// integer helpers:
|
||||
|
||||
macro_rules! integer_helper {
|
||||
($ty:ident, $deserialize_name:ident, $trait: ident, $from_name:ident, $visitor:ident) => {
|
||||
pub trait $trait: Sized + Default {
|
||||
fn $from_name(value: $ty) -> Self;
|
||||
}
|
||||
|
||||
impl $trait for $ty {
|
||||
fn $from_name(value: $ty) -> Self {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
impl $trait for Option<$ty> {
|
||||
fn $from_name(value: $ty) -> Self {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn $deserialize_name<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: $trait,
|
||||
{
|
||||
deserializer.deserialize_any($visitor::<T>::new())
|
||||
}
|
||||
|
||||
struct $visitor<T>(std::marker::PhantomData<T>);
|
||||
|
||||
impl<T> $visitor<T> {
|
||||
fn new() -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: $trait> serde::de::DeserializeSeed<'de> for $visitor<T> {
|
||||
type Value = T;
|
||||
|
||||
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
$deserialize_name(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> serde::de::Visitor<'de> for $visitor<T>
|
||||
where
|
||||
T: $trait,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(concat!("a ", stringify!($ty), "-ish..."))
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(self)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
|
||||
$ty::try_from(value)
|
||||
.map_err(|_| E::invalid_value(Unexpected::Other("i128"), &self))
|
||||
.map(Self::Value::$from_name)
|
||||
}
|
||||
|
||||
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
|
||||
$ty::try_from(value)
|
||||
.map_err(|_| E::invalid_value(Unexpected::Signed(value), &self))
|
||||
.map(Self::Value::$from_name)
|
||||
}
|
||||
|
||||
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
|
||||
$ty::try_from(value)
|
||||
.map_err(|_| E::invalid_value(Unexpected::Unsigned(value), &self))
|
||||
.map(Self::Value::$from_name)
|
||||
}
|
||||
|
||||
fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
|
||||
$ty::try_from(value)
|
||||
.map_err(|_| E::invalid_value(Unexpected::Other("u128"), &self))
|
||||
.map(Self::Value::$from_name)
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
let value = value
|
||||
.parse()
|
||||
.map_err(|_| E::invalid_value(Unexpected::Str(value), &self))?;
|
||||
self.visit_i64(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
integer_helper!(
|
||||
isize,
|
||||
deserialize_isize,
|
||||
FromIsize,
|
||||
from_isize,
|
||||
IsizeVisitor
|
||||
);
|
||||
|
||||
integer_helper!(
|
||||
usize,
|
||||
deserialize_usize,
|
||||
FromUsize,
|
||||
from_usize,
|
||||
UsizeVisitor
|
||||
);
|
||||
|
||||
integer_helper!(u8, deserialize_u8, FromU8, from_u8, U8Visitor);
|
||||
integer_helper!(u16, deserialize_u16, FromU16, from_u16, U16Visitor);
|
||||
integer_helper!(u32, deserialize_u32, FromU32, from_u32, U32Visitor);
|
||||
integer_helper!(u64, deserialize_u64, FromU64, from_u64, U64Visitor);
|
||||
integer_helper!(i8, deserialize_i8, FromI8, from_i8, I8Visitor);
|
||||
integer_helper!(i16, deserialize_i16, FromI16, from_i16, I16Visitor);
|
||||
integer_helper!(i32, deserialize_i32, FromI32, from_i32, I32Visitor);
|
||||
integer_helper!(i64, deserialize_i64, FromI64, from_i64, I64Visitor);
|
128
proxmox-login/src/tfa.rs
Normal file
128
proxmox-login/src/tfa.rs
Normal file
@ -0,0 +1,128 @@
|
||||
//! These types are from the `proxmox-tfa` crate. Currently the 'api' feature is required for this,
|
||||
//! but we should add a feature that exposes the types without the api implementation and drop the
|
||||
//! types from here.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// When sending a TFA challenge to the user, we include information about what kind of challenge
|
||||
/// the user may perform. If webauthn credentials are available, a webauthn challenge will be
|
||||
/// included.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TfaChallenge {
|
||||
/// True if the user has TOTP devices.
|
||||
#[serde(skip_serializing_if = "bool_is_false", default)]
|
||||
pub totp: bool,
|
||||
|
||||
/// Whether there are recovery keys available.
|
||||
#[serde(skip_serializing_if = "RecoveryState::is_unavailable", default)]
|
||||
pub recovery: RecoveryState,
|
||||
|
||||
#[cfg(feature = "webauthn")]
|
||||
/// If the user has any webauthn credentials registered, this will contain the corresponding
|
||||
/// challenge data.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub webauthn: Option<webauthn_rs::proto::RequestChallengeResponse>,
|
||||
|
||||
/// True if the user has yubico keys configured.
|
||||
#[serde(skip_serializing_if = "bool_is_false", default)]
|
||||
pub yubico: bool,
|
||||
}
|
||||
|
||||
fn bool_is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
/// Used to inform the user about the recovery code status.
|
||||
///
|
||||
/// This contains the available key indices.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct RecoveryState(Vec<usize>);
|
||||
|
||||
impl RecoveryState {
|
||||
pub fn is_available(&self) -> bool {
|
||||
!self.is_unavailable()
|
||||
}
|
||||
|
||||
pub fn is_unavailable(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// The "key" part of a registration, passed to `u2f.sign` in the registered keys list.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase` and base64url without padding.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisteredKey {
|
||||
/// Identifies the key handle on the client side. Used to create authentication challenges, so
|
||||
/// the client knows which key to use. Must be remembered.
|
||||
#[serde(with = "bytes_as_base64url_nopad")]
|
||||
pub key_handle: Vec<u8>,
|
||||
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
mod bytes_as_base64url_nopad {
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
|
||||
use serde::de::Error;
|
||||
String::deserialize(deserializer).and_then(|string| {
|
||||
base64::decode_config(&string, base64::URL_SAFE_NO_PAD)
|
||||
.map_err(|err| Error::custom(err.to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A user's response to a TFA challenge.
|
||||
pub enum TfaResponse {
|
||||
Totp(String),
|
||||
U2f(serde_json::Value),
|
||||
Webauthn(serde_json::Value),
|
||||
Recovery(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InvalidTfaResponse {
|
||||
Unknown,
|
||||
BadJson(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidTfaResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
InvalidTfaResponse::Unknown => f.write_str("unrecognized tfa response type"),
|
||||
InvalidTfaResponse::BadJson(err) => fmt::Display::fmt(err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidTfaResponse {}
|
||||
|
||||
impl From<serde_json::Error> for InvalidTfaResponse {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
InvalidTfaResponse::BadJson(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is part of the REST API:
|
||||
impl std::str::FromStr for TfaResponse {
|
||||
type Err = InvalidTfaResponse;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(if let Some(totp) = s.strip_prefix("totp:") {
|
||||
TfaResponse::Totp(totp.to_string())
|
||||
} else if let Some(u2f) = s.strip_prefix("u2f:") {
|
||||
TfaResponse::U2f(serde_json::from_str(u2f)?)
|
||||
} else if let Some(webauthn) = s.strip_prefix("webauthn:") {
|
||||
TfaResponse::Webauthn(serde_json::from_str(webauthn)?)
|
||||
} else if let Some(recovery) = s.strip_prefix("recovery:") {
|
||||
TfaResponse::Recovery(recovery.to_string())
|
||||
} else {
|
||||
return Err(InvalidTfaResponse::Unknown);
|
||||
})
|
||||
}
|
||||
}
|
252
proxmox-login/src/ticket.rs
Normal file
252
proxmox-login/src/ticket.rs
Normal file
@ -0,0 +1,252 @@
|
||||
//! Ticket related data.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::TicketError;
|
||||
use crate::tfa::TfaChallenge;
|
||||
|
||||
/// The repsonse to a ticket call can either be a complete ticket, or a TFA challenge.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum TicketResponse {
|
||||
Full(Ticket),
|
||||
Tfa(String, TfaChallenge),
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TicketResponse {
|
||||
type Err = TicketError;
|
||||
|
||||
fn from_str(ticket: &str) -> Result<Self, TicketError> {
|
||||
let pos = ticket.find(':').ok_or(TicketError)?;
|
||||
match ticket[pos..].strip_prefix(":!tfa!") {
|
||||
Some(challenge) => match challenge.find(':') {
|
||||
Some(pos) => {
|
||||
let challenge: std::borrow::Cow<[u8]> =
|
||||
percent_encoding::percent_decode_str(&challenge[..pos]).into();
|
||||
let challenge = serde_json::from_slice(&challenge).map_err(|_| TicketError)?;
|
||||
Ok(TicketResponse::Tfa(ticket.to_string(), challenge))
|
||||
}
|
||||
None => Err(TicketError),
|
||||
},
|
||||
None => ticket.parse().map(TicketResponse::Full),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An API ticket string. Serializable so it can be stored for later reuse.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ticket {
|
||||
data: Box<str>,
|
||||
timestamp: i64,
|
||||
product_len: u16,
|
||||
userid_len: u16,
|
||||
// timestamp_len: u16,
|
||||
}
|
||||
|
||||
/// Tickets are valid for 2 hours.
|
||||
const TICKET_LIFETIME: i64 = 2 * 3600;
|
||||
/// We refresh during the last half hour.
|
||||
const REFRESH_EARLY_BY: i64 = 1800;
|
||||
|
||||
impl Ticket {
|
||||
/// The ticket's product prefix.
|
||||
pub fn product(&self) -> &str {
|
||||
&self.data[..usize::from(self.product_len)]
|
||||
}
|
||||
|
||||
/// The userid contained in the ticket.
|
||||
pub fn userid(&self) -> &str {
|
||||
let start = usize::from(self.product_len) + 1;
|
||||
let len = usize::from(self.userid_len);
|
||||
&self.data[start..(start + len)]
|
||||
}
|
||||
|
||||
/// Thet ticket's timestamp as a UNIX epoch.
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// The ticket age in seconds.
|
||||
pub fn age(&self) -> i64 {
|
||||
epoch_i64() - self.timestamp
|
||||
}
|
||||
|
||||
/// This is a convenience check for the ticket's validity assuming the usual ticket lifetime of
|
||||
/// 2 hours.
|
||||
pub fn validity(&self) -> Validity {
|
||||
let age = self.age();
|
||||
if age > TICKET_LIFETIME {
|
||||
Validity::Expired
|
||||
} else if age >= TICKET_LIFETIME - REFRESH_EARLY_BY {
|
||||
Validity::Refresh
|
||||
} else {
|
||||
Validity::Valid
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cookie in the form `<PRODUCT>AuthCookie=Ticket`.
|
||||
pub fn cookie(&self) -> String {
|
||||
format!("{}AuthCookie={}", self.product(), self.data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a ticket should be refreshed or is already invalid and needs to be completely renewed.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Validity {
|
||||
/// The ticket is still valid for longer than half an hour.
|
||||
Valid,
|
||||
|
||||
/// The ticket is within its final half hour validity period and should be renewed with the
|
||||
/// ticket as password.
|
||||
Refresh,
|
||||
|
||||
/// The ticket is already invalid and a new ticket needs to be created.
|
||||
Expired,
|
||||
}
|
||||
|
||||
impl Validity {
|
||||
/// Simply check whether the ticket is considered valid even if it should be renewed.
|
||||
pub fn is_valid(self) -> bool {
|
||||
matches!(self, Validity::Valid | Validity::Refresh)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Ticket {
|
||||
type Err = TicketError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, TicketError> {
|
||||
let data = s;
|
||||
|
||||
// get product:
|
||||
let product_len = s.find(':').ok_or(TicketError)?;
|
||||
if product_len >= 10 {
|
||||
// weird product
|
||||
return Err(TicketError);
|
||||
}
|
||||
let s = &s[(product_len + 1)..];
|
||||
|
||||
// get userid:
|
||||
let userid_len = s.find(':').ok_or(TicketError)?;
|
||||
if !s[..userid_len].contains('@') {
|
||||
return Err(TicketError);
|
||||
}
|
||||
let s = &s[(userid_len + 1)..];
|
||||
|
||||
// timestamp
|
||||
let timestamp_len = s.find(':').ok_or(TicketError)?;
|
||||
let timestamp = i64::from_str_radix(&s[..timestamp_len], 16).map_err(|_| TicketError)?;
|
||||
|
||||
let s = &s[(timestamp_len + 1)..];
|
||||
|
||||
let s = s.strip_prefix(':').ok_or(TicketError)?;
|
||||
if s.is_empty() {
|
||||
return Err(TicketError);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
product_len: u16::try_from(product_len).map_err(|_| TicketError)?,
|
||||
userid_len: u16::try_from(userid_len).map_err(|_| TicketError)?,
|
||||
//timestamp_len: u16::try_from(timestamp_len).map_err(|_| TicketError)?,
|
||||
timestamp,
|
||||
data: data.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ticket {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ticket> for String {
|
||||
fn from(ticket: Ticket) -> String {
|
||||
ticket.data.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ticket> for Box<str> {
|
||||
fn from(ticket: Ticket) -> Box<str> {
|
||||
ticket.data
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Ticket {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Ticket {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
std::borrow::Cow::<'de, str>::deserialize(deserializer)?
|
||||
.parse()
|
||||
.map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// A finished authentication state.
|
||||
///
|
||||
/// This is serializable / deserializable in order to be able to easily store it.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Authentication {
|
||||
/// The API URL this authentication info belongs to.
|
||||
pub api_url: String,
|
||||
|
||||
/// The user id in the form of `username@realm`.
|
||||
pub userid: String,
|
||||
|
||||
/// The authentication ticket.
|
||||
pub ticket: Ticket,
|
||||
|
||||
/// The cluster name (if any)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub clustername: Option<String>,
|
||||
|
||||
/// The CSRFPreventionToken header.
|
||||
#[serde(rename = "CSRFPreventionToken")]
|
||||
pub csrfprevention_token: String,
|
||||
}
|
||||
|
||||
impl Authentication {
|
||||
/// Get the ticket cookie in the form `<PRODUCT>AuthCookie=Ticket`.
|
||||
pub fn cookie(&self) -> String {
|
||||
self.ticket.cookie()
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
/// Add authentication headers to a request.
|
||||
///
|
||||
/// This is equivalent to doing:
|
||||
/// ```ignore
|
||||
/// request
|
||||
/// .header(http::header::COOKIE, auth.cookie())
|
||||
/// .header(proxmox_login::CSRF_HEADER_NAME, &auth.csrfprevention_token)
|
||||
/// ```
|
||||
pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder {
|
||||
request
|
||||
.header(http::header::COOKIE, self.cookie())
|
||||
.header(crate::CSRF_HEADER_NAME, &self.csrfprevention_token)
|
||||
}
|
||||
}
|
||||
|
||||
fn epoch_i64() -> i64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now();
|
||||
if now > UNIX_EPOCH {
|
||||
i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs()).unwrap_or(0)
|
||||
} else {
|
||||
-i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs()).unwrap_or(0)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user