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:
parent
45636cce1a
commit
d97ff8ae2a
@ -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
10
debian/control
vendored
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
|
@ -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(()) => (),
|
||||
|
@ -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)),
|
||||
)?;
|
||||
|
||||
|
240
src/auth.rs
240
src/auth.rs
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
|
@ -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()?;
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ pub mod api2;
|
||||
|
||||
pub mod auth_helpers;
|
||||
|
||||
pub mod auth;
|
||||
pub(crate) mod auth;
|
||||
|
||||
pub mod tape;
|
||||
|
||||
|
@ -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 _))
|
||||
}
|
||||
|
@ -31,8 +31,6 @@ pub use email_notifications::*;
|
||||
mod report;
|
||||
pub use report::*;
|
||||
|
||||
pub mod ticket;
|
||||
|
||||
pub mod auth;
|
||||
|
||||
pub(crate) mod pull;
|
||||
|
@ -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()?))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user