5
0
mirror of git://git.proxmox.com/git/proxmox-backup.git synced 2025-02-09 09:57:40 +03:00

use new auth api crate

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2023-02-01 16:01:12 +01:00 committed by Thomas Lamprecht
parent 45636cce1a
commit d97ff8ae2a
23 changed files with 232 additions and 906 deletions

View File

@ -37,7 +37,6 @@ members = [
"pbs-key-config",
"pbs-pxar-fuse",
"pbs-tape",
"pbs-ticket",
"pbs-tools",
"proxmox-backup-banner",
@ -56,6 +55,7 @@ path = "src/lib.rs"
[workspace.dependencies]
# proxmox workspace
proxmox-async = "0.4"
proxmox-auth-api = "0.1"
proxmox-borrow = "1"
proxmox-compression = "0.1.1"
proxmox-fuse = "0.1.3"
@ -75,7 +75,7 @@ proxmox-shared-memory = "0.2.3"
proxmox-sortable-macro = "0.1.2"
proxmox-subscription = { version = "0.3", features = [ "api-types" ] }
proxmox-sys = "0.4.2"
proxmox-tfa = { version = "2.1", features = [ "api", "api-types" ] }
proxmox-tfa = { version = "3", features = [ "api", "api-types" ] }
proxmox-time = "1.1.2"
proxmox-uuid = "1"
@ -96,7 +96,6 @@ pbs-fuse-loop = { path = "pbs-fuse-loop" }
pbs-key-config = { path = "pbs-key-config" }
pbs-pxar-fuse = { path = "pbs-pxar-fuse" }
pbs-tape = { path = "pbs-tape" }
pbs-ticket = { path = "pbs-ticket" }
pbs-tools = { path = "pbs-tools" }
proxmox-rrd = { path = "proxmox-rrd" }
@ -203,6 +202,7 @@ zstd.workspace = true
#valgrind_request = { git = "https://github.com/edef1c/libvalgrind_request", version = "1.1.0", optional = true }
proxmox-async.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api", "pam-authenticator" ] }
proxmox-compression.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
proxmox-io.workspace = true
@ -235,7 +235,6 @@ pbs-config.workspace = true
pbs-datastore.workspace = true
pbs-key-config.workspace = true
pbs-tape.workspace = true
pbs-ticket.workspace = true
pbs-tools.workspace = true
proxmox-rrd.workspace = true
@ -244,6 +243,7 @@ proxmox-rrd.workspace = true
[patch.crates-io]
#proxmox-acme-rs = { path = "../proxmox-acme-rs" }
#proxmox-async = { path = "../proxmox/proxmox-async" }
#proxmox-auth-api = { path = "../proxmox/proxmox-auth-api" }
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
#proxmox-fuse = { path = "../proxmox-fuse" }

10
debian/control vendored
View File

@ -44,6 +44,10 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-acme-rs-0.4+default-dev,
librust-proxmox-apt-0.9+default-dev,
librust-proxmox-async-0.4+default-dev,
librust-proxmox-auth-api-0.1+api-dev,
librust-proxmox-auth-api-0.1+api-types-dev,
librust-proxmox-auth-api-0.1+default-dev,
librust-proxmox-auth-api-0.1+pam-authenticator-dev,
librust-proxmox-borrow-1+default-dev,
librust-proxmox-compression-0.1+default-dev (>= 0.1.1-~~),
librust-proxmox-fuse-0.1+default-dev (>= 0.1.3-~~),
@ -81,9 +85,9 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-sys-0.4+default-dev (>= 0.4.2-~~),
librust-proxmox-sys-0.4+logrotate-dev (>= 0.4.2-~~),
librust-proxmox-sys-0.4+timer-dev (>= 0.4.2-~~),
librust-proxmox-tfa-2+api-dev (>= 2.1-~~),
librust-proxmox-tfa-2+api-types-dev (>= 2.1-~~),
librust-proxmox-tfa-2+default-dev (>= 2.1-~~),
librust-proxmox-tfa-3+api-dev,
librust-proxmox-tfa-3+api-types-dev,
librust-proxmox-tfa-3+default-dev,
librust-proxmox-time-1+default-dev (>= 1.1.2-~~),
librust-proxmox-uuid-1+default-dev,
librust-proxmox-uuid-1+serde-dev,

View File

@ -14,6 +14,7 @@ regex.workspace = true
serde.workspace = true
serde_plain.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api-types" ] }
proxmox-lang.workspace=true
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
proxmox-serde.workspace = true

View File

@ -2,6 +2,8 @@
use serde::{Deserialize, Serialize};
use proxmox_auth_api::{APITOKEN_ID_REGEX_STR, USER_ID_REGEX_STR};
pub mod common_regex;
pub mod percent_encoding;
@ -85,14 +87,14 @@ pub use maintenance::*;
mod network;
pub use network::*;
#[macro_use]
mod userid;
pub use userid::Authid;
pub use userid::Userid;
pub use userid::{Realm, RealmRef};
pub use userid::{Tokenname, TokennameRef};
pub use userid::{Username, UsernameRef};
pub use userid::{PROXMOX_GROUP_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA};
pub use proxmox_auth_api::types as userid;
pub use proxmox_auth_api::types::{Authid, Userid};
pub use proxmox_auth_api::types::{Realm, RealmRef};
pub use proxmox_auth_api::types::{Tokenname, TokennameRef};
pub use proxmox_auth_api::types::{Username, UsernameRef};
pub use proxmox_auth_api::types::{
PROXMOX_GROUP_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA,
};
#[macro_use]
mod user;

View File

@ -34,6 +34,7 @@ xdg.workspace = true
pathpatterns.workspace = true
proxmox-async.workspace = true
proxmox-auth-api.workspace = true
proxmox-compression.workspace = true
proxmox-http = { workspace = true, features = [ "rate-limiter" ] }
proxmox-io = { workspace = true, features = [ "tokio" ] }
@ -48,5 +49,4 @@ pxar.workspace = true
pbs-api-types.workspace = true
pbs-buildcfg.workspace = true
pbs-datastore.workspace = true
pbs-ticket.workspace = true
pbs-tools.workspace = true

View File

@ -249,7 +249,7 @@ fn store_ticket_info(
let mut new_data = json!({});
let ticket_lifetime = pbs_ticket::TICKET_LIFETIME - 60;
let ticket_lifetime = proxmox_auth_api::TICKET_LIFETIME - 60;
let empty = serde_json::map::Map::new();
for (server, info) in data.as_object().unwrap_or(&empty) {
@ -280,7 +280,7 @@ fn load_ticket_info(prefix: &str, server: &str, userid: &Userid) -> Option<(Stri
let path = base.place_runtime_file("tickets").ok()?;
let data = file_get_json(&path, None).ok()?;
let now = proxmox_time::epoch_i64();
let ticket_lifetime = pbs_ticket::TICKET_LIFETIME - 60;
let ticket_lifetime = proxmox_auth_api::TICKET_LIFETIME - 60;
let uinfo = data[server][userid.as_str()].as_object()?;
let timestamp = uinfo["timestamp"].as_i64()?;
let age = now - timestamp;

View File

@ -1,14 +0,0 @@
[package]
name = "pbs-ticket"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
description = "pbs ticket handling"
[dependencies]
anyhow.workspace = true
base64.workspace = true
openssl.workspace = true
percent-encoding.workspace = true
proxmox-time.workspace = true

View File

@ -1,332 +0,0 @@
//! Generate and verify Authentication tickets
use std::borrow::Cow;
use std::io;
use std::marker::PhantomData;
use anyhow::{bail, format_err, Error};
use openssl::hash::MessageDigest;
use openssl::pkey::{HasPublic, PKey, Private};
use openssl::sign::{Signer, Verifier};
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
pub const TERM_PREFIX: &str = "PBSTERM";
/// Stringified ticket data must not contain colons...
const TICKET_ASCIISET: &AsciiSet = &percent_encoding::CONTROLS.add(b':');
/// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
/// with no data.
pub struct Empty;
impl ToString for Empty {
fn to_string(&self) -> String {
String::new()
}
}
impl std::str::FromStr for Empty {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if !s.is_empty() {
bail!("unexpected ticket data, should be empty");
}
Ok(Empty)
}
}
/// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
/// authenticaztion data, a timestamp and a signature. We store these values in the form
/// `<prefix>:<stringified data>:<timestamp>::<signature>`.
///
/// The signature is made over the string consisting of prefix, data, timestamp and aad joined
/// together by colons. If there is no additional authentication data it will be skipped together
/// with the colon separating it from the timestamp.
pub struct Ticket<T>
where
T: ToString + std::str::FromStr,
{
prefix: Cow<'static, str>,
data: String,
time: i64,
signature: Option<Vec<u8>>,
_type_marker: PhantomData<fn() -> T>,
}
impl<T> Ticket<T>
where
T: ToString + std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
/// Prepare a new ticket for signing.
pub fn new(prefix: &'static str, data: &T) -> Result<Self, Error> {
Ok(Self {
prefix: Cow::Borrowed(prefix),
data: data.to_string(),
time: proxmox_time::epoch_i64(),
signature: None,
_type_marker: PhantomData,
})
}
/// Get the ticket prefix.
pub fn prefix(&self) -> &str {
&self.prefix
}
/// Get the ticket's time stamp in seconds since the unix epoch.
pub fn time(&self) -> i64 {
self.time
}
/// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
/// this in the end, so using this method directly is discouraged as it does not verify the
/// signature.
pub fn raw_data(&self) -> &str {
&self.data
}
/// Serialize the ticket into a writer.
///
/// This only writes a string. We use `io::write` instead of `fmt::Write` so we can reuse the
/// same function for openssl's `Verify`, which only implements `io::Write`.
fn write_data(&self, f: &mut dyn io::Write) -> Result<(), Error> {
write!(
f,
"{}:{}:{:08X}",
percent_encode(self.prefix.as_bytes(), TICKET_ASCIISET),
percent_encode(self.data.as_bytes(), TICKET_ASCIISET),
self.time,
)
.map_err(Error::from)
}
/// Write additional authentication data to the verifier.
fn write_aad(f: &mut dyn io::Write, aad: Option<&str>) -> Result<(), Error> {
if let Some(aad) = aad {
write!(f, ":{}", percent_encode(aad.as_bytes(), TICKET_ASCIISET))?;
}
Ok(())
}
/// Change the ticket's time, used mostly for testing.
#[cfg(test)]
fn change_time(&mut self, time: i64) -> &mut Self {
self.time = time;
self
}
/// Sign the ticket.
pub fn sign(&mut self, keypair: &PKey<Private>, aad: Option<&str>) -> Result<String, Error> {
let mut output = Vec::<u8>::new();
let mut signer = Signer::new(MessageDigest::sha256(), keypair)
.map_err(|err| format_err!("openssl error creating signer for ticket: {}", err))?;
self.write_data(&mut output)
.map_err(|err| format_err!("error creating ticket: {}", err))?;
signer
.update(&output)
.map_err(Error::from)
.and_then(|()| Self::write_aad(&mut signer, aad))
.map_err(|err| format_err!("error signing ticket: {}", err))?;
// See `Self::write_data` for why this is safe
let mut output = unsafe { String::from_utf8_unchecked(output) };
let signature = signer
.sign_to_vec()
.map_err(|err| format_err!("error finishing ticket signature: {}", err))?;
use std::fmt::Write;
write!(
&mut output,
"::{}",
base64::encode_config(&signature, base64::STANDARD_NO_PAD),
)?;
self.signature = Some(signature);
Ok(output)
}
/// `verify` with an additional time frame parameter, not usually required since we always use
/// the same time frame.
pub fn verify_with_time_frame<P: HasPublic>(
&self,
keypair: &PKey<P>,
prefix: &str,
aad: Option<&str>,
time_frame: std::ops::Range<i64>,
) -> Result<T, Error> {
if self.prefix != prefix {
bail!("ticket with invalid prefix");
}
let signature = match self.signature.as_ref() {
Some(sig) => sig,
None => bail!("invalid ticket without signature"),
};
let age = proxmox_time::epoch_i64() - self.time;
if age < time_frame.start {
bail!("invalid ticket - timestamp newer than expected");
}
if age > time_frame.end {
bail!("invalid ticket - expired");
}
let mut verifier = Verifier::new(MessageDigest::sha256(), keypair)?;
self.write_data(&mut verifier)
.and_then(|()| Self::write_aad(&mut verifier, aad))
.map_err(|err| format_err!("error verifying ticket: {}", err))?;
let is_valid: bool = verifier
.verify(signature)
.map_err(|err| format_err!("openssl error verifying ticket: {}", err))?;
if !is_valid {
bail!("ticket with invalid signature");
}
self.data
.parse()
.map_err(|err| format_err!("failed to parse contained ticket data: {:?}", err))
}
/// Verify the ticket with the provided key pair. The additional authentication data needs to
/// match the one used when generating the ticket, and the ticket's age must fall into the time
/// frame.
pub fn verify<P: HasPublic>(
&self,
keypair: &PKey<P>,
prefix: &str,
aad: Option<&str>,
) -> Result<T, Error> {
self.verify_with_time_frame(keypair, prefix, aad, -300..TICKET_LIFETIME)
}
/// Parse a ticket string.
pub fn parse(ticket: &str) -> Result<Self, Error> {
let mut parts = ticket.splitn(4, ':');
let prefix = percent_decode_str(
parts
.next()
.ok_or_else(|| format_err!("ticket without prefix"))?,
)
.decode_utf8()
.map_err(|err| format_err!("invalid ticket, error decoding prefix: {}", err))?;
let data = percent_decode_str(
parts
.next()
.ok_or_else(|| format_err!("ticket without data"))?,
)
.decode_utf8()
.map_err(|err| format_err!("invalid ticket, error decoding data: {}", err))?;
let time = i64::from_str_radix(
parts
.next()
.ok_or_else(|| format_err!("ticket without timestamp"))?,
16,
)
.map_err(|err| format_err!("ticket with bad timestamp: {}", err))?;
let remainder = parts
.next()
.ok_or_else(|| format_err!("ticket without signature"))?;
// <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
// double-colon!
if !remainder.starts_with(':') {
bail!("ticket without signature separator");
}
let signature = base64::decode_config(&remainder[1..], base64::STANDARD_NO_PAD)
.map_err(|err| format_err!("ticket with bad signature: {}", err))?;
Ok(Self {
prefix: Cow::Owned(prefix.into_owned()),
data: data.into_owned(),
time,
signature: Some(signature),
_type_marker: PhantomData,
})
}
}
#[cfg(test)]
mod test {
use std::convert::Infallible;
use std::fmt;
use openssl::pkey::{PKey, Private};
use super::Ticket;
#[derive(Debug, Eq, PartialEq)]
struct Testid(String);
impl std::str::FromStr for Testid {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Infallible> {
Ok(Self(s.to_string()))
}
}
impl fmt::Display for Testid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
fn simple_test<F>(key: &PKey<Private>, aad: Option<&str>, modify: F)
where
F: FnOnce(&mut Ticket<Testid>) -> bool,
{
let userid = Testid("root".to_string());
let mut ticket = Ticket::new("PREFIX", &userid).expect("failed to create Ticket struct");
let should_work = modify(&mut ticket);
let ticket = ticket.sign(key, aad).expect("failed to sign test ticket");
let parsed =
Ticket::<Testid>::parse(&ticket).expect("failed to parse generated test ticket");
if should_work {
let check: Testid = parsed
.verify(key, "PREFIX", aad)
.expect("failed to verify test ticket");
assert_eq!(userid, check);
} else {
parsed
.verify(key, "PREFIX", aad)
.expect_err("failed to verify test ticket");
}
}
#[test]
fn test_tickets() {
// first we need keys, for testing we use small keys for speed...
let rsa =
openssl::rsa::Rsa::generate(1024).expect("failed to generate RSA key for testing");
let key = openssl::pkey::PKey::<openssl::pkey::Private>::from_rsa(rsa)
.expect("failed to create PKey for RSA key");
simple_test(&key, Some("secret aad data"), |_| true);
simple_test(&key, None, |_| true);
simple_test(&key, None, |t| {
t.change_time(0);
false
});
simple_test(&key, None, |t| {
t.change_time(proxmox_time::epoch_i64() + 0x1000_0000);
false
});
}
}

View File

@ -2,13 +2,11 @@
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
use proxmox_router::{
http_err, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
};
use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
@ -18,11 +16,6 @@ use pbs_api_types::{
};
use pbs_config::acl::AclTreeNode;
use pbs_config::CachedUserInfo;
use pbs_ticket::{Empty, Ticket};
use crate::auth_helpers::*;
use crate::config::tfa::TfaChallenge;
use crate::server::ticket::ApiTicket;
pub mod acl;
pub mod domain;
@ -31,213 +24,6 @@ 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(Box<TfaChallenge>),
}
async fn authenticate_user(
userid: &Userid,
password: &str,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
tfa_challenge: Option<String>,
) -> Result<AuthResult, Error> {
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::<Userid>::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(),
pbs_ticket::TERM_PREFIX,
Some(&crate::tools::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.is_empty() {
path_vec.push(part);
}
}
user_info.check_privs(&auth_id, &path_vec, *privilege, false)?;
return Ok(AuthResult::Success);
}
}
bail!("No such privilege");
}
}
#[allow(clippy::let_unit_value)]
{
let _: () = crate::auth::authenticate_user(userid, password).await?;
}
Ok(match crate::config::tfa::login_challenge(userid)? {
None => AuthResult::CreateTicket,
Some(challenge) => AuthResult::Partial(Box::new(challenge)),
})
}
fn authenticate_2nd(
userid: &Userid,
challenge_ticket: &str,
response: &str,
) -> Result<AuthResult, Error> {
let challenge: Box<TfaChallenge> = Ticket::<ApiTicket>::parse(challenge_ticket)?
.verify_with_time_frame(public_auth_key(), "PBS", Some(userid.as_str()), -60..600)?
.require_partial()?;
#[allow(clippy::let_unit_value)]
{
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 async fn create_ticket(
username: Userid,
password: String,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
tfa_challenge: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
use proxmox_rest_server::RestEnvironment;
let env: &RestEnvironment = rpcenv
.as_any()
.downcast_ref::<RestEnvironment>()
.ok_or_else(|| format_err!("detected wrong RpcEnvironment type"))?;
match authenticate_user(&username, &password, path, privs, port, tfa_challenge).await {
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);
env.log_auth(username.as_str());
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) => {
env.log_failed_auth(Some(username.to_string()), &err.to_string());
Err(http_err!(UNAUTHORIZED, "permission check failed."))
}
}
}
#[api(
protected: true,
input: {
@ -425,7 +211,10 @@ const SUBDIRS: SubdirMap = &sorted!([
"permissions",
&Router::new().get(&API_METHOD_LIST_PERMISSIONS)
),
("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)),
(
"ticket",
&Router::new().post(&proxmox_auth_api::api::API_METHOD_CREATE_TICKET)
),
("openid", &openid::ROUTER),
("domains", &domain::ROUTER),
("roles", &role::ROUTER),

View File

@ -2,6 +2,8 @@
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox_auth_api::api::ApiTicket;
use proxmox_auth_api::ticket::Ticket;
use proxmox_router::{
http_err, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
};
@ -15,13 +17,12 @@ use pbs_api_types::{
OPENID_DEFAILT_SCOPE_LIST, REALM_ID_SCHEMA,
};
use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
use pbs_ticket::Ticket;
use pbs_config::open_backup_lockfile;
use pbs_config::CachedUserInfo;
use crate::auth::auth_keyring;
use crate::auth_helpers::*;
use crate::server::ticket::ApiTicket;
fn openid_authenticator(
realm_config: &OpenIdRealmConfig,
@ -199,7 +200,7 @@ pub fn openid_login(
}
let api_ticket = ApiTicket::Full(user_id.clone());
let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
let ticket = Ticket::new("PBS", &api_ticket)?.sign(auth_keyring(), None)?;
let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
env.log_auth(user_id.as_str());

View File

@ -225,7 +225,7 @@ async fn add_tfa_entry(
let mut data = crate::config::tfa::read()?;
let out = methods::add_tfa_entry(
&mut data,
UserAccess,
&UserAccess,
userid.as_str(),
description,
totp,

View File

@ -377,7 +377,7 @@ pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error>
match crate::config::tfa::read().and_then(|mut cfg| {
let _: proxmox_tfa::api::NeedsSaving =
cfg.remove_user(crate::config::tfa::UserAccess, userid.as_str())?;
cfg.remove_user(&crate::config::tfa::UserAccess, userid.as_str())?;
crate::config::tfa::write(&cfg)
}) {
Ok(()) => (),

View File

@ -12,22 +12,21 @@ use hyper::Request;
use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, BufReader};
use proxmox_sys::fd::fd_change_cloexec;
use proxmox_sortable_macro::sortable;
use proxmox_auth_api::ticket::{Empty, Ticket};
use proxmox_auth_api::types::Authid;
use proxmox_http::websocket::WebSocket;
use proxmox_rest_server::WorkerTask;
use proxmox_router::list_subdirs_api_method;
use proxmox_router::{
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
};
use proxmox_schema::*;
use proxmox_sortable_macro::sortable;
use proxmox_sys::fd::fd_change_cloexec;
use proxmox_rest_server::WorkerTask;
use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_CONSOLE};
use pbs_api_types::{Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE};
use pbs_ticket::{Empty, Ticket};
use crate::auth_helpers::private_auth_key;
use crate::auth::auth_keyring;
use crate::tools;
pub mod apt;
@ -119,8 +118,8 @@ async fn termproxy(cmd: Option<String>, rpcenv: &mut dyn RpcEnvironment) -> Resu
let listener = TcpListener::bind("localhost:0")?;
let port = listener.local_addr()?.port();
let ticket = Ticket::new(pbs_ticket::TERM_PREFIX, &Empty)?.sign(
private_auth_key(),
let ticket = Ticket::new(crate::auth::TERM_PREFIX, &Empty)?.sign(
auth_keyring(),
Some(&tools::ticket::term_aad(userid, path, port)),
)?;
@ -291,8 +290,8 @@ fn upgrade_to_websocket(
// will be checked again by termproxy
Ticket::<Empty>::parse(ticket)?.verify(
crate::auth_helpers::public_auth_key(),
pbs_ticket::TERM_PREFIX,
auth_keyring(),
crate::auth::TERM_PREFIX,
Some(&tools::ticket::term_aad(userid, "/system", port)),
)?;

View File

@ -2,99 +2,34 @@
//!
//! This library contains helper to authenticate users.
use std::io::Write;
use std::path::PathBuf;
use std::pin::Pin;
use std::process::{Command, Stdio};
use anyhow::{bail, format_err, Error};
use anyhow::{bail, Error};
use futures::Future;
use once_cell::sync::OnceCell;
use proxmox_router::http_bail;
use serde_json::json;
use proxmox_auth_api::api::{Authenticator, LockedTfaConfig};
use proxmox_auth_api::ticket::{Empty, Ticket};
use proxmox_auth_api::types::Authid;
use proxmox_auth_api::Keyring;
use proxmox_ldap::{Config, Connection, ConnectionMode};
use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
use pbs_api_types::{LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef};
use pbs_buildcfg::configdir;
use crate::auth_helpers;
use proxmox_ldap::{Config, Connection, ConnectionMode};
pub trait ProxmoxAuthenticator {
fn authenticate_user<'a>(
&'a self,
username: &'a UsernameRef,
password: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
fn remove_password(&self, username: &UsernameRef) -> Result<(), Error>;
}
pub const TERM_PREFIX: &str = "PBSTERM";
struct PamAuthenticator();
impl ProxmoxAuthenticator for PamAuthenticator {
fn authenticate_user<'a>(
&self,
username: &'a UsernameRef,
password: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
Box::pin(async move {
let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap();
auth.get_handler()
.set_credentials(username.as_str(), password);
auth.authenticate()?;
Ok(())
})
}
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error> {
let mut child = Command::new("passwd")
.arg(username.as_str())
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| {
format_err!(
"unable to set password for '{}' - execute passwd failed: {}",
username.as_str(),
err,
)
})?;
// Note: passwd reads password twice from stdin (for verify)
writeln!(child.stdin.as_mut().unwrap(), "{}\n{}", password, password)?;
let output = child.wait_with_output().map_err(|err| {
format_err!(
"unable to set password for '{}' - wait failed: {}",
username.as_str(),
err,
)
})?;
if !output.status.success() {
bail!(
"unable to set password for '{}' - {}",
username.as_str(),
String::from_utf8_lossy(&output.stderr),
);
}
Ok(())
}
// do not remove password for pam users
fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
http_bail!(
NOT_IMPLEMENTED,
"removing passwords is not implemented for PAM realms"
);
}
}
struct PbsAuthenticator();
struct PbsAuthenticator;
const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
impl ProxmoxAuthenticator for PbsAuthenticator {
impl Authenticator for PbsAuthenticator {
fn authenticate_user<'a>(
&self,
username: &'a UsernameRef,
@ -150,7 +85,7 @@ struct OpenIdAuthenticator();
/// When a user is manually added, the lookup_authenticator is called to verify that
/// the realm exists. Thus, it is necessary to have an (empty) implementation for
/// OpendID as well.
impl ProxmoxAuthenticator for OpenIdAuthenticator {
impl Authenticator for OpenIdAuthenticator {
fn authenticate_user<'a>(
&'a self,
_username: &'a UsernameRef,
@ -184,7 +119,7 @@ pub struct LdapAuthenticator {
config: LdapRealmConfig,
}
impl ProxmoxAuthenticator for LdapAuthenticator {
impl Authenticator for LdapAuthenticator {
/// Authenticate user in LDAP realm
fn authenticate_user<'a>(
&'a self,
@ -254,12 +189,12 @@ impl LdapAuthenticator {
}
/// Lookup the autenticator for the specified realm
pub fn lookup_authenticator(
pub(crate) fn lookup_authenticator(
realm: &RealmRef,
) -> Result<Box<dyn ProxmoxAuthenticator + Send + Sync + 'static>, Error> {
) -> Result<Box<dyn Authenticator + Send + Sync>, Error> {
match realm.as_str() {
"pam" => Ok(Box::new(PamAuthenticator())),
"pbs" => Ok(Box::new(PbsAuthenticator())),
"pam" => Ok(Box::new(proxmox_auth_api::Pam::new("proxmox-backup-auth"))),
"pbs" => Ok(Box::new(PbsAuthenticator)),
realm => {
let (domains, _digest) = pbs_config::domains::config()?;
if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
@ -274,7 +209,7 @@ pub fn lookup_authenticator(
}
/// Authenticate users
pub fn authenticate_user<'a>(
pub(crate) fn authenticate_user<'a>(
userid: &'a Userid,
password: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
@ -285,3 +220,140 @@ pub fn authenticate_user<'a>(
Ok(())
})
}
static AUTH_CONTEXT: OnceCell<PbsAuthContext> = OnceCell::new();
pub fn setup_auth_context(use_private_key: bool) {
let keyring = if use_private_key {
Keyring::with_private_key(crate::auth_helpers::private_auth_key().clone().into())
} else {
Keyring::with_public_key(crate::auth_helpers::public_auth_key().clone().into())
};
AUTH_CONTEXT
.set(PbsAuthContext {
keyring,
csrf_secret: crate::auth_helpers::csrf_secret().to_vec(),
})
.map_err(drop)
.expect("auth context setup twice");
proxmox_auth_api::set_auth_context(AUTH_CONTEXT.get().unwrap());
}
pub(crate) fn auth_keyring() -> &'static Keyring {
&AUTH_CONTEXT
.get()
.expect("setup_auth_context not called")
.keyring
}
struct PbsAuthContext {
keyring: Keyring,
csrf_secret: Vec<u8>,
}
impl proxmox_auth_api::api::AuthContext for PbsAuthContext {
fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>> {
lookup_authenticator(realm).ok()
}
/// Get the current authentication keyring.
fn keyring(&self) -> &Keyring {
&self.keyring
}
/// The auth prefix without the separating colon. Eg. `"PBS"`.
fn auth_prefix(&self) -> &'static str {
"PBS"
}
/// API token prefix (without the `'='`).
fn auth_token_prefix(&self) -> &'static str {
"PBSAPIToken"
}
/// Auth cookie name.
fn auth_cookie_name(&self) -> &'static str {
"PBSAuthCookie"
}
/// Check if a userid is enabled and return a [`UserInformation`] handle.
fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error> {
Ok(pbs_config::CachedUserInfo::new()?.is_active_auth_id(auth_id))
}
/// Access the TFA config with an exclusive lock.
fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error> {
Ok(Box::new(PbsLockedTfaConfig {
_lock: crate::config::tfa::read_lock()?,
config: crate::config::tfa::read()?,
}))
}
/// CSRF prevention token secret data.
fn csrf_secret(&self) -> &[u8] {
&self.csrf_secret
}
/// Verify a token secret.
fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error> {
pbs_config::token_shadow::verify_secret(token_id, token_secret)
}
/// Check path based tickets. (Used for terminal tickets).
fn check_path_ticket(
&self,
userid: &Userid,
password: &str,
path: String,
privs: String,
port: u16,
) -> Result<Option<bool>, Error> {
if !password.starts_with("PBSTERM:") {
return Ok(None);
}
if let Ok(Empty) = Ticket::parse(password).and_then(|ticket| {
ticket.verify(
&self.keyring,
TERM_PREFIX,
Some(&crate::tools::ticket::term_aad(userid, &path, port)),
)
}) {
let user_info = pbs_config::CachedUserInfo::new()?;
let auth_id = Authid::from(userid.clone());
for (name, privilege) in pbs_api_types::PRIVILEGES {
if *name == privs {
let mut path_vec = Vec::new();
for part in path.split('/') {
if !part.is_empty() {
path_vec.push(part);
}
}
user_info.check_privs(&auth_id, &path_vec, *privilege, false)?;
return Ok(Some(true));
}
}
}
Ok(Some(false))
}
}
struct PbsLockedTfaConfig {
_lock: pbs_config::BackupLockGuard,
config: TfaConfig,
}
static USER_ACCESS: crate::config::tfa::UserAccess = crate::config::tfa::UserAccess;
impl LockedTfaConfig for PbsLockedTfaConfig {
fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig) {
(&USER_ACCESS, &mut self.config)
}
fn save_config(&mut self) -> Result<(), Error> {
crate::config::tfa::write(&self.config)
}
}

View File

@ -14,6 +14,8 @@ use pbs_api_types::Userid;
use pbs_buildcfg::configdir;
use serde_json::json;
pub use crate::auth::setup_auth_context;
fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
let mut hasher = sha::Sha256::new();
let data = format!("{:08X}:{}:", timestamp, userid);

View File

@ -71,6 +71,8 @@ async fn run() -> Result<(), Error> {
}
let _ = csrf_secret(); // load with lazy_static
proxmox_backup::auth_helpers::setup_auth_context(true);
let backup_user = pbs_config::backup_user()?;
let mut commando_sock = proxmox_rest_server::CommandSocket::new(
proxmox_rest_server::our_ctrl_sock(),

View File

@ -176,8 +176,7 @@ async fn run() -> Result<(), Error> {
bail!("unable to inititialize syslog - {err}");
}
let _ = public_auth_key(); // load with lazy_static
let _ = csrf_secret(); // load with lazy_static
proxmox_backup::auth_helpers::setup_auth_context(false);
let rrd_cache = initialize_rrd_cache()?;
rrd_cache.apply_journal()?;

View File

@ -2,17 +2,17 @@ use anyhow::Error;
use pbs_api_types::{Authid, Userid};
use pbs_client::{HttpClient, HttpClientOptions};
use pbs_ticket::Ticket;
use crate::auth_helpers::private_auth_key;
use proxmox_auth_api::ticket::Ticket;
use crate::auth::auth_keyring;
/// Connect to localhost:8007 as root@pam
///
/// This automatically creates a ticket if run as 'root' user.
pub fn connect_to_localhost() -> Result<pbs_client::HttpClient, Error> {
let options = if nix::unistd::Uid::current().is_root() {
let auth_key = private_auth_key();
let ticket = Ticket::new("PBS", Userid::root_userid())?.sign(auth_key, None)?;
let ticket = Ticket::new("PBS", Userid::root_userid())?.sign(auth_keyring(), None)?;
let fingerprint = crate::cert_info()?.fingerprint()?;
HttpClientOptions::new_non_interactive(ticket, Some(fingerprint))
} else {

View File

@ -12,7 +12,8 @@ use proxmox_sys::fs::CreateOptions;
use proxmox_tfa::totp::Totp;
pub use proxmox_tfa::api::{
TfaChallenge, TfaConfig, TfaResponse, WebauthnConfig, WebauthnConfigUpdater,
TfaChallenge, TfaConfig, TfaResponse, UserChallengeAccess, WebauthnConfig,
WebauthnConfigUpdater,
};
use pbs_api_types::{User, Userid};
@ -110,10 +111,7 @@ impl TfaUserChallengeData {
/// itself, as it is in `/run`, and the typical error case for this particular situation
/// (machine loses power) simply prevents some login, but that'll probably fail anyway for
/// other reasons then...
///
/// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
/// way also unlocks early.
fn save(mut self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
self.rewind()?;
serde_json::to_writer(io::BufWriter::new(&mut &self.lock), &self.inner).map_err(|err| {
@ -124,12 +122,6 @@ impl TfaUserChallengeData {
}
}
/// Get an optional TFA challenge for a user.
pub fn login_challenge(userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
let _lock = write_lock()?;
read()?.authentication_challenge(UserAccess, userid.as_str(), None)
}
/// Add a TOTP entry for a user. Returns the ID.
pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result<String, Error> {
let _lock = write_lock();
@ -153,7 +145,7 @@ pub fn add_recovery(userid: &Userid) -> Result<Vec<String>, Error> {
pub fn add_u2f_registration(userid: &Userid, description: String) -> Result<String, Error> {
let _lock = crate::config::tfa::write_lock();
let mut data = read()?;
let challenge = data.u2f_registration_challenge(UserAccess, userid.as_str(), description)?;
let challenge = data.u2f_registration_challenge(&UserAccess, userid.as_str(), description)?;
write(&data)?;
Ok(challenge)
}
@ -166,7 +158,7 @@ pub fn finish_u2f_registration(
) -> Result<String, Error> {
let _lock = crate::config::tfa::write_lock();
let mut data = read()?;
let id = data.u2f_registration_finish(UserAccess, userid.as_str(), challenge, response)?;
let id = data.u2f_registration_finish(&UserAccess, userid.as_str(), challenge, response)?;
write(&data)?;
Ok(id)
}
@ -176,7 +168,7 @@ pub fn add_webauthn_registration(userid: &Userid, description: String) -> Result
let _lock = crate::config::tfa::write_lock();
let mut data = read()?;
let challenge =
data.webauthn_registration_challenge(UserAccess, userid.as_str(), description, None)?;
data.webauthn_registration_challenge(&UserAccess, userid.as_str(), description, None)?;
write(&data)?;
Ok(challenge)
}
@ -190,39 +182,20 @@ pub fn finish_webauthn_registration(
let _lock = crate::config::tfa::write_lock();
let mut data = read()?;
let id =
data.webauthn_registration_finish(UserAccess, userid.as_str(), challenge, response, None)?;
data.webauthn_registration_finish(&UserAccess, userid.as_str(), challenge, response, None)?;
write(&data)?;
Ok(id)
}
/// Verify a TFA challenge.
pub fn verify_challenge(
userid: &Userid,
challenge: &TfaChallenge,
response: TfaResponse,
) -> Result<(), Error> {
let _lock = crate::config::tfa::write_lock();
let mut data = read()?;
if data
.verify(UserAccess, userid.as_str(), challenge, response, None)?
.needs_saving()
{
write(&data)?;
}
Ok(())
}
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct UserAccess;
/// Build th
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
type Data = TfaUserChallengeData;
/// Load the user's current challenges with the intent to create a challenge (create the file
/// if it does not exist), and keep a lock on the file.
fn open(&self, userid: &str) -> Result<Self::Data, Error> {
fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
crate::server::create_run_dir()?;
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
proxmox_sys::fs::create_path(CHALLENGE_DATA_PATH, Some(options.clone()), Some(options))
@ -269,15 +242,15 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
}
};
Ok(TfaUserChallengeData {
Ok(Box::new(TfaUserChallengeData {
inner,
path,
lock: file,
})
}))
}
/// `open` without creating the file if it doesn't exist, to finish WA authentications.
fn open_no_create(&self, userid: &str) -> Result<Option<Self::Data>, Error> {
fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
let path = challenge_data_path_str(userid);
let mut file = match std::fs::OpenOptions::new()
.read(true)
@ -297,11 +270,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
format_err!("failed to read challenge data for user {}: {}", userid, err)
})?;
Ok(Some(TfaUserChallengeData {
Ok(Some(Box::new(TfaUserChallengeData {
inner,
path,
lock: file,
}))
})))
}
/// `remove` user data if it exists.
@ -320,7 +293,7 @@ impl proxmox_tfa::api::UserChallengeAccess for TfaUserChallengeData {
&mut self.inner
}
fn save(self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
TfaUserChallengeData::save(self)
}
}

View File

@ -23,7 +23,7 @@ pub mod api2;
pub mod auth_helpers;
pub mod auth;
pub(crate) mod auth;
pub mod tape;

View File

@ -1,115 +1,13 @@
//! Provides authentication primitives for the HTTP server
use anyhow::format_err;
use proxmox_rest_server::AuthError;
use proxmox_router::UserInformation;
use pbs_api_types::{Authid, Userid};
use pbs_config::{token_shadow, CachedUserInfo};
use pbs_ticket::Ticket;
use proxmox_rest_server::{extract_cookie, AuthError};
use crate::auth_helpers::*;
use hyper::header;
use percent_encoding::percent_decode_str;
struct UserAuthData {
ticket: String,
csrf_token: Option<String>,
}
enum AuthData {
User(UserAuthData),
ApiToken(String),
}
fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> {
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()))
} else {
None
}
}
_ => None,
}
}
use pbs_config::CachedUserInfo;
pub async fn check_pbs_auth(
headers: &http::HeaderMap,
method: &hyper::Method,
) -> Result<(String, Box<dyn UserInformation + Sync + Send>), 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 = pbs_ticket::TICKET_LIFETIME;
let userid: Userid = Ticket::<super::ticket::ApiTicket>::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)))
}
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"))?;
token_shadow::verify_secret(&tokenid, &tokensecret)?;
Ok((tokenid.to_string(), Box::new(user_info)))
}
None => Err(AuthError::NoData),
}
proxmox_auth_api::api::http_check_auth(headers, method)
.map(move |name| (name, Box::new(user_info) as _))
}

View File

@ -31,8 +31,6 @@ pub use email_notifications::*;
mod report;
pub use report::*;
pub mod ticket;
pub mod auth;
pub(crate) mod pull;

View File

@ -1,68 +0,0 @@
use std::fmt;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use pbs_api_types::Userid;
use crate::config::tfa;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PartialTicket {
#[serde(rename = "u")]
userid: Userid,
#[serde(rename = "c")]
challenge: tfa::TfaChallenge,
}
/// A new ticket struct used in rest.rs's `check_auth` - mostly for better errors than failing to
/// parse the userid ticket content.
pub enum ApiTicket {
Full(Userid),
Partial(Box<tfa::TfaChallenge>),
}
impl ApiTicket {
/// Require the ticket to be a full ticket, otherwise error with a meaningful error message.
pub fn require_full(self) -> Result<Userid, Error> {
match self {
ApiTicket::Full(userid) => Ok(userid),
ApiTicket::Partial(_) => bail!("access denied - second login factor required"),
}
}
/// Expect the ticket to contain a tfa challenge, otherwise error with a meaningful error
/// message.
pub fn require_partial(self) -> Result<Box<tfa::TfaChallenge>, Error> {
match self {
ApiTicket::Full(_) => bail!("invalid tfa challenge"),
ApiTicket::Partial(challenge) => Ok(challenge),
}
}
}
impl fmt::Display for ApiTicket {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ApiTicket::Full(userid) => fmt::Display::fmt(userid, f),
ApiTicket::Partial(partial) => {
let data = serde_json::to_string(partial).map_err(|_| fmt::Error)?;
write!(f, "!tfa!{}", data)
}
}
}
}
impl std::str::FromStr for ApiTicket {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if let Some(tfa_ticket) = s.strip_prefix("!tfa!") {
Ok(ApiTicket::Partial(serde_json::from_str(tfa_ticket)?))
} else {
Ok(ApiTicket::Full(s.parse()?))
}
}
}