mirror of
https://git.proxmox.com/git/proxmox-backup
synced 2025-04-29 22:45:19 +00: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-key-config",
|
||||||
"pbs-pxar-fuse",
|
"pbs-pxar-fuse",
|
||||||
"pbs-tape",
|
"pbs-tape",
|
||||||
"pbs-ticket",
|
|
||||||
"pbs-tools",
|
"pbs-tools",
|
||||||
|
|
||||||
"proxmox-backup-banner",
|
"proxmox-backup-banner",
|
||||||
@ -56,6 +55,7 @@ path = "src/lib.rs"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# proxmox workspace
|
# proxmox workspace
|
||||||
proxmox-async = "0.4"
|
proxmox-async = "0.4"
|
||||||
|
proxmox-auth-api = "0.1"
|
||||||
proxmox-borrow = "1"
|
proxmox-borrow = "1"
|
||||||
proxmox-compression = "0.1.1"
|
proxmox-compression = "0.1.1"
|
||||||
proxmox-fuse = "0.1.3"
|
proxmox-fuse = "0.1.3"
|
||||||
@ -75,7 +75,7 @@ proxmox-shared-memory = "0.2.3"
|
|||||||
proxmox-sortable-macro = "0.1.2"
|
proxmox-sortable-macro = "0.1.2"
|
||||||
proxmox-subscription = { version = "0.3", features = [ "api-types" ] }
|
proxmox-subscription = { version = "0.3", features = [ "api-types" ] }
|
||||||
proxmox-sys = "0.4.2"
|
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-time = "1.1.2"
|
||||||
proxmox-uuid = "1"
|
proxmox-uuid = "1"
|
||||||
|
|
||||||
@ -96,7 +96,6 @@ pbs-fuse-loop = { path = "pbs-fuse-loop" }
|
|||||||
pbs-key-config = { path = "pbs-key-config" }
|
pbs-key-config = { path = "pbs-key-config" }
|
||||||
pbs-pxar-fuse = { path = "pbs-pxar-fuse" }
|
pbs-pxar-fuse = { path = "pbs-pxar-fuse" }
|
||||||
pbs-tape = { path = "pbs-tape" }
|
pbs-tape = { path = "pbs-tape" }
|
||||||
pbs-ticket = { path = "pbs-ticket" }
|
|
||||||
pbs-tools = { path = "pbs-tools" }
|
pbs-tools = { path = "pbs-tools" }
|
||||||
proxmox-rrd = { path = "proxmox-rrd" }
|
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 }
|
#valgrind_request = { git = "https://github.com/edef1c/libvalgrind_request", version = "1.1.0", optional = true }
|
||||||
|
|
||||||
proxmox-async.workspace = true
|
proxmox-async.workspace = true
|
||||||
|
proxmox-auth-api = { workspace = true, features = [ "api", "pam-authenticator" ] }
|
||||||
proxmox-compression.workspace = true
|
proxmox-compression.workspace = true
|
||||||
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
|
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
|
||||||
proxmox-io.workspace = true
|
proxmox-io.workspace = true
|
||||||
@ -235,7 +235,6 @@ pbs-config.workspace = true
|
|||||||
pbs-datastore.workspace = true
|
pbs-datastore.workspace = true
|
||||||
pbs-key-config.workspace = true
|
pbs-key-config.workspace = true
|
||||||
pbs-tape.workspace = true
|
pbs-tape.workspace = true
|
||||||
pbs-ticket.workspace = true
|
|
||||||
pbs-tools.workspace = true
|
pbs-tools.workspace = true
|
||||||
proxmox-rrd.workspace = true
|
proxmox-rrd.workspace = true
|
||||||
|
|
||||||
@ -244,6 +243,7 @@ proxmox-rrd.workspace = true
|
|||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
#proxmox-acme-rs = { path = "../proxmox-acme-rs" }
|
#proxmox-acme-rs = { path = "../proxmox-acme-rs" }
|
||||||
#proxmox-async = { path = "../proxmox/proxmox-async" }
|
#proxmox-async = { path = "../proxmox/proxmox-async" }
|
||||||
|
#proxmox-auth-api = { path = "../proxmox/proxmox-auth-api" }
|
||||||
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
|
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
|
||||||
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
|
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
|
||||||
#proxmox-fuse = { path = "../proxmox-fuse" }
|
#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-acme-rs-0.4+default-dev,
|
||||||
librust-proxmox-apt-0.9+default-dev,
|
librust-proxmox-apt-0.9+default-dev,
|
||||||
librust-proxmox-async-0.4+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-borrow-1+default-dev,
|
||||||
librust-proxmox-compression-0.1+default-dev (>= 0.1.1-~~),
|
librust-proxmox-compression-0.1+default-dev (>= 0.1.1-~~),
|
||||||
librust-proxmox-fuse-0.1+default-dev (>= 0.1.3-~~),
|
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+default-dev (>= 0.4.2-~~),
|
||||||
librust-proxmox-sys-0.4+logrotate-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-sys-0.4+timer-dev (>= 0.4.2-~~),
|
||||||
librust-proxmox-tfa-2+api-dev (>= 2.1-~~),
|
librust-proxmox-tfa-3+api-dev,
|
||||||
librust-proxmox-tfa-2+api-types-dev (>= 2.1-~~),
|
librust-proxmox-tfa-3+api-types-dev,
|
||||||
librust-proxmox-tfa-2+default-dev (>= 2.1-~~),
|
librust-proxmox-tfa-3+default-dev,
|
||||||
librust-proxmox-time-1+default-dev (>= 1.1.2-~~),
|
librust-proxmox-time-1+default-dev (>= 1.1.2-~~),
|
||||||
librust-proxmox-uuid-1+default-dev,
|
librust-proxmox-uuid-1+default-dev,
|
||||||
librust-proxmox-uuid-1+serde-dev,
|
librust-proxmox-uuid-1+serde-dev,
|
||||||
|
@ -14,6 +14,7 @@ regex.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_plain.workspace = true
|
serde_plain.workspace = true
|
||||||
|
|
||||||
|
proxmox-auth-api = { workspace = true, features = [ "api-types" ] }
|
||||||
proxmox-lang.workspace=true
|
proxmox-lang.workspace=true
|
||||||
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
|
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
|
||||||
proxmox-serde.workspace = true
|
proxmox-serde.workspace = true
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use proxmox_auth_api::{APITOKEN_ID_REGEX_STR, USER_ID_REGEX_STR};
|
||||||
|
|
||||||
pub mod common_regex;
|
pub mod common_regex;
|
||||||
pub mod percent_encoding;
|
pub mod percent_encoding;
|
||||||
|
|
||||||
@ -85,14 +87,14 @@ pub use maintenance::*;
|
|||||||
mod network;
|
mod network;
|
||||||
pub use network::*;
|
pub use network::*;
|
||||||
|
|
||||||
#[macro_use]
|
pub use proxmox_auth_api::types as userid;
|
||||||
mod userid;
|
pub use proxmox_auth_api::types::{Authid, Userid};
|
||||||
pub use userid::Authid;
|
pub use proxmox_auth_api::types::{Realm, RealmRef};
|
||||||
pub use userid::Userid;
|
pub use proxmox_auth_api::types::{Tokenname, TokennameRef};
|
||||||
pub use userid::{Realm, RealmRef};
|
pub use proxmox_auth_api::types::{Username, UsernameRef};
|
||||||
pub use userid::{Tokenname, TokennameRef};
|
pub use proxmox_auth_api::types::{
|
||||||
pub use userid::{Username, UsernameRef};
|
PROXMOX_GROUP_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA,
|
||||||
pub use userid::{PROXMOX_GROUP_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA};
|
};
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod user;
|
mod user;
|
||||||
|
@ -34,6 +34,7 @@ xdg.workspace = true
|
|||||||
pathpatterns.workspace = true
|
pathpatterns.workspace = true
|
||||||
|
|
||||||
proxmox-async.workspace = true
|
proxmox-async.workspace = true
|
||||||
|
proxmox-auth-api.workspace = true
|
||||||
proxmox-compression.workspace = true
|
proxmox-compression.workspace = true
|
||||||
proxmox-http = { workspace = true, features = [ "rate-limiter" ] }
|
proxmox-http = { workspace = true, features = [ "rate-limiter" ] }
|
||||||
proxmox-io = { workspace = true, features = [ "tokio" ] }
|
proxmox-io = { workspace = true, features = [ "tokio" ] }
|
||||||
@ -48,5 +49,4 @@ pxar.workspace = true
|
|||||||
pbs-api-types.workspace = true
|
pbs-api-types.workspace = true
|
||||||
pbs-buildcfg.workspace = true
|
pbs-buildcfg.workspace = true
|
||||||
pbs-datastore.workspace = true
|
pbs-datastore.workspace = true
|
||||||
pbs-ticket.workspace = true
|
|
||||||
pbs-tools.workspace = true
|
pbs-tools.workspace = true
|
||||||
|
@ -249,7 +249,7 @@ fn store_ticket_info(
|
|||||||
|
|
||||||
let mut new_data = json!({});
|
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();
|
let empty = serde_json::map::Map::new();
|
||||||
for (server, info) in data.as_object().unwrap_or(&empty) {
|
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 path = base.place_runtime_file("tickets").ok()?;
|
||||||
let data = file_get_json(&path, None).ok()?;
|
let data = file_get_json(&path, None).ok()?;
|
||||||
let now = proxmox_time::epoch_i64();
|
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 uinfo = data[server][userid.as_str()].as_object()?;
|
||||||
let timestamp = uinfo["timestamp"].as_i64()?;
|
let timestamp = uinfo["timestamp"].as_i64()?;
|
||||||
let age = now - timestamp;
|
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 anyhow::{bail, format_err, Error};
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use proxmox_router::{
|
use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
|
||||||
http_err, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
|
|
||||||
};
|
|
||||||
use proxmox_schema::api;
|
use proxmox_schema::api;
|
||||||
use proxmox_sortable_macro::sortable;
|
use proxmox_sortable_macro::sortable;
|
||||||
|
|
||||||
@ -18,11 +16,6 @@ use pbs_api_types::{
|
|||||||
};
|
};
|
||||||
use pbs_config::acl::AclTreeNode;
|
use pbs_config::acl::AclTreeNode;
|
||||||
use pbs_config::CachedUserInfo;
|
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 acl;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
@ -31,213 +24,6 @@ pub mod role;
|
|||||||
pub mod tfa;
|
pub mod tfa;
|
||||||
pub mod user;
|
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(
|
#[api(
|
||||||
protected: true,
|
protected: true,
|
||||||
input: {
|
input: {
|
||||||
@ -425,7 +211,10 @@ const SUBDIRS: SubdirMap = &sorted!([
|
|||||||
"permissions",
|
"permissions",
|
||||||
&Router::new().get(&API_METHOD_LIST_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),
|
("openid", &openid::ROUTER),
|
||||||
("domains", &domain::ROUTER),
|
("domains", &domain::ROUTER),
|
||||||
("roles", &role::ROUTER),
|
("roles", &role::ROUTER),
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use proxmox_auth_api::api::ApiTicket;
|
||||||
|
use proxmox_auth_api::ticket::Ticket;
|
||||||
use proxmox_router::{
|
use proxmox_router::{
|
||||||
http_err, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
|
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,
|
OPENID_DEFAILT_SCOPE_LIST, REALM_ID_SCHEMA,
|
||||||
};
|
};
|
||||||
use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
|
use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
|
||||||
use pbs_ticket::Ticket;
|
|
||||||
|
|
||||||
use pbs_config::open_backup_lockfile;
|
use pbs_config::open_backup_lockfile;
|
||||||
use pbs_config::CachedUserInfo;
|
use pbs_config::CachedUserInfo;
|
||||||
|
|
||||||
|
use crate::auth::auth_keyring;
|
||||||
use crate::auth_helpers::*;
|
use crate::auth_helpers::*;
|
||||||
use crate::server::ticket::ApiTicket;
|
|
||||||
|
|
||||||
fn openid_authenticator(
|
fn openid_authenticator(
|
||||||
realm_config: &OpenIdRealmConfig,
|
realm_config: &OpenIdRealmConfig,
|
||||||
@ -199,7 +200,7 @@ pub fn openid_login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let api_ticket = ApiTicket::Full(user_id.clone());
|
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);
|
let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
|
||||||
|
|
||||||
env.log_auth(user_id.as_str());
|
env.log_auth(user_id.as_str());
|
||||||
|
@ -225,7 +225,7 @@ async fn add_tfa_entry(
|
|||||||
let mut data = crate::config::tfa::read()?;
|
let mut data = crate::config::tfa::read()?;
|
||||||
let out = methods::add_tfa_entry(
|
let out = methods::add_tfa_entry(
|
||||||
&mut data,
|
&mut data,
|
||||||
UserAccess,
|
&UserAccess,
|
||||||
userid.as_str(),
|
userid.as_str(),
|
||||||
description,
|
description,
|
||||||
totp,
|
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| {
|
match crate::config::tfa::read().and_then(|mut cfg| {
|
||||||
let _: proxmox_tfa::api::NeedsSaving =
|
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)
|
crate::config::tfa::write(&cfg)
|
||||||
}) {
|
}) {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
|
@ -12,22 +12,21 @@ use hyper::Request;
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
|
||||||
use proxmox_sys::fd::fd_change_cloexec;
|
use proxmox_auth_api::ticket::{Empty, Ticket};
|
||||||
use proxmox_sortable_macro::sortable;
|
use proxmox_auth_api::types::Authid;
|
||||||
|
|
||||||
use proxmox_http::websocket::WebSocket;
|
use proxmox_http::websocket::WebSocket;
|
||||||
|
use proxmox_rest_server::WorkerTask;
|
||||||
use proxmox_router::list_subdirs_api_method;
|
use proxmox_router::list_subdirs_api_method;
|
||||||
use proxmox_router::{
|
use proxmox_router::{
|
||||||
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
|
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
|
||||||
};
|
};
|
||||||
use proxmox_schema::*;
|
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 crate::auth::auth_keyring;
|
||||||
use pbs_ticket::{Empty, Ticket};
|
|
||||||
|
|
||||||
use crate::auth_helpers::private_auth_key;
|
|
||||||
use crate::tools;
|
use crate::tools;
|
||||||
|
|
||||||
pub mod apt;
|
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 listener = TcpListener::bind("localhost:0")?;
|
||||||
let port = listener.local_addr()?.port();
|
let port = listener.local_addr()?.port();
|
||||||
|
|
||||||
let ticket = Ticket::new(pbs_ticket::TERM_PREFIX, &Empty)?.sign(
|
let ticket = Ticket::new(crate::auth::TERM_PREFIX, &Empty)?.sign(
|
||||||
private_auth_key(),
|
auth_keyring(),
|
||||||
Some(&tools::ticket::term_aad(userid, path, port)),
|
Some(&tools::ticket::term_aad(userid, path, port)),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -291,8 +290,8 @@ fn upgrade_to_websocket(
|
|||||||
|
|
||||||
// will be checked again by termproxy
|
// will be checked again by termproxy
|
||||||
Ticket::<Empty>::parse(ticket)?.verify(
|
Ticket::<Empty>::parse(ticket)?.verify(
|
||||||
crate::auth_helpers::public_auth_key(),
|
auth_keyring(),
|
||||||
pbs_ticket::TERM_PREFIX,
|
crate::auth::TERM_PREFIX,
|
||||||
Some(&tools::ticket::term_aad(userid, "/system", port)),
|
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.
|
//! This library contains helper to authenticate users.
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, Error};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use proxmox_router::http_bail;
|
use proxmox_router::http_bail;
|
||||||
use serde_json::json;
|
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_api_types::{LdapMode, LdapRealmConfig, OpenIdRealmConfig, RealmRef, Userid, UsernameRef};
|
||||||
use pbs_buildcfg::configdir;
|
use pbs_buildcfg::configdir;
|
||||||
|
|
||||||
use crate::auth_helpers;
|
use crate::auth_helpers;
|
||||||
use proxmox_ldap::{Config, Connection, ConnectionMode};
|
|
||||||
|
|
||||||
pub trait ProxmoxAuthenticator {
|
pub const TERM_PREFIX: &str = "PBSTERM";
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PamAuthenticator();
|
struct PbsAuthenticator;
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
|
const SHADOW_CONFIG_FILENAME: &str = configdir!("/shadow.json");
|
||||||
|
|
||||||
impl ProxmoxAuthenticator for PbsAuthenticator {
|
impl Authenticator for PbsAuthenticator {
|
||||||
fn authenticate_user<'a>(
|
fn authenticate_user<'a>(
|
||||||
&self,
|
&self,
|
||||||
username: &'a UsernameRef,
|
username: &'a UsernameRef,
|
||||||
@ -150,7 +85,7 @@ struct OpenIdAuthenticator();
|
|||||||
/// When a user is manually added, the lookup_authenticator is called to verify that
|
/// 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
|
/// the realm exists. Thus, it is necessary to have an (empty) implementation for
|
||||||
/// OpendID as well.
|
/// OpendID as well.
|
||||||
impl ProxmoxAuthenticator for OpenIdAuthenticator {
|
impl Authenticator for OpenIdAuthenticator {
|
||||||
fn authenticate_user<'a>(
|
fn authenticate_user<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_username: &'a UsernameRef,
|
_username: &'a UsernameRef,
|
||||||
@ -184,7 +119,7 @@ pub struct LdapAuthenticator {
|
|||||||
config: LdapRealmConfig,
|
config: LdapRealmConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxmoxAuthenticator for LdapAuthenticator {
|
impl Authenticator for LdapAuthenticator {
|
||||||
/// Authenticate user in LDAP realm
|
/// Authenticate user in LDAP realm
|
||||||
fn authenticate_user<'a>(
|
fn authenticate_user<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
@ -254,12 +189,12 @@ impl LdapAuthenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup the autenticator for the specified realm
|
/// Lookup the autenticator for the specified realm
|
||||||
pub fn lookup_authenticator(
|
pub(crate) fn lookup_authenticator(
|
||||||
realm: &RealmRef,
|
realm: &RealmRef,
|
||||||
) -> Result<Box<dyn ProxmoxAuthenticator + Send + Sync + 'static>, Error> {
|
) -> Result<Box<dyn Authenticator + Send + Sync>, Error> {
|
||||||
match realm.as_str() {
|
match realm.as_str() {
|
||||||
"pam" => Ok(Box::new(PamAuthenticator())),
|
"pam" => Ok(Box::new(proxmox_auth_api::Pam::new("proxmox-backup-auth"))),
|
||||||
"pbs" => Ok(Box::new(PbsAuthenticator())),
|
"pbs" => Ok(Box::new(PbsAuthenticator)),
|
||||||
realm => {
|
realm => {
|
||||||
let (domains, _digest) = pbs_config::domains::config()?;
|
let (domains, _digest) = pbs_config::domains::config()?;
|
||||||
if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
|
if let Ok(config) = domains.lookup::<LdapRealmConfig>("ldap", realm) {
|
||||||
@ -274,7 +209,7 @@ pub fn lookup_authenticator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate users
|
/// Authenticate users
|
||||||
pub fn authenticate_user<'a>(
|
pub(crate) fn authenticate_user<'a>(
|
||||||
userid: &'a Userid,
|
userid: &'a Userid,
|
||||||
password: &'a str,
|
password: &'a str,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
|
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
|
||||||
@ -285,3 +220,140 @@ pub fn authenticate_user<'a>(
|
|||||||
Ok(())
|
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 pbs_buildcfg::configdir;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub use crate::auth::setup_auth_context;
|
||||||
|
|
||||||
fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
|
fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
|
||||||
let mut hasher = sha::Sha256::new();
|
let mut hasher = sha::Sha256::new();
|
||||||
let data = format!("{:08X}:{}:", timestamp, userid);
|
let data = format!("{:08X}:{}:", timestamp, userid);
|
||||||
|
@ -71,6 +71,8 @@ async fn run() -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
let _ = csrf_secret(); // load with lazy_static
|
let _ = csrf_secret(); // load with lazy_static
|
||||||
|
|
||||||
|
proxmox_backup::auth_helpers::setup_auth_context(true);
|
||||||
|
|
||||||
let backup_user = pbs_config::backup_user()?;
|
let backup_user = pbs_config::backup_user()?;
|
||||||
let mut commando_sock = proxmox_rest_server::CommandSocket::new(
|
let mut commando_sock = proxmox_rest_server::CommandSocket::new(
|
||||||
proxmox_rest_server::our_ctrl_sock(),
|
proxmox_rest_server::our_ctrl_sock(),
|
||||||
|
@ -176,8 +176,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
bail!("unable to inititialize syslog - {err}");
|
bail!("unable to inititialize syslog - {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = public_auth_key(); // load with lazy_static
|
proxmox_backup::auth_helpers::setup_auth_context(false);
|
||||||
let _ = csrf_secret(); // load with lazy_static
|
|
||||||
|
|
||||||
let rrd_cache = initialize_rrd_cache()?;
|
let rrd_cache = initialize_rrd_cache()?;
|
||||||
rrd_cache.apply_journal()?;
|
rrd_cache.apply_journal()?;
|
||||||
|
@ -2,17 +2,17 @@ use anyhow::Error;
|
|||||||
|
|
||||||
use pbs_api_types::{Authid, Userid};
|
use pbs_api_types::{Authid, Userid};
|
||||||
use pbs_client::{HttpClient, HttpClientOptions};
|
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
|
/// Connect to localhost:8007 as root@pam
|
||||||
///
|
///
|
||||||
/// This automatically creates a ticket if run as 'root' user.
|
/// This automatically creates a ticket if run as 'root' user.
|
||||||
pub fn connect_to_localhost() -> Result<pbs_client::HttpClient, Error> {
|
pub fn connect_to_localhost() -> Result<pbs_client::HttpClient, Error> {
|
||||||
let options = if nix::unistd::Uid::current().is_root() {
|
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_keyring(), None)?;
|
||||||
let ticket = Ticket::new("PBS", Userid::root_userid())?.sign(auth_key, None)?;
|
|
||||||
let fingerprint = crate::cert_info()?.fingerprint()?;
|
let fingerprint = crate::cert_info()?.fingerprint()?;
|
||||||
HttpClientOptions::new_non_interactive(ticket, Some(fingerprint))
|
HttpClientOptions::new_non_interactive(ticket, Some(fingerprint))
|
||||||
} else {
|
} else {
|
||||||
|
@ -12,7 +12,8 @@ use proxmox_sys::fs::CreateOptions;
|
|||||||
use proxmox_tfa::totp::Totp;
|
use proxmox_tfa::totp::Totp;
|
||||||
|
|
||||||
pub use proxmox_tfa::api::{
|
pub use proxmox_tfa::api::{
|
||||||
TfaChallenge, TfaConfig, TfaResponse, WebauthnConfig, WebauthnConfigUpdater,
|
TfaChallenge, TfaConfig, TfaResponse, UserChallengeAccess, WebauthnConfig,
|
||||||
|
WebauthnConfigUpdater,
|
||||||
};
|
};
|
||||||
|
|
||||||
use pbs_api_types::{User, Userid};
|
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
|
/// 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
|
/// (machine loses power) simply prevents some login, but that'll probably fail anyway for
|
||||||
/// other reasons then...
|
/// other reasons then...
|
||||||
///
|
fn save(&mut self) -> Result<(), Error> {
|
||||||
/// 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> {
|
|
||||||
self.rewind()?;
|
self.rewind()?;
|
||||||
|
|
||||||
serde_json::to_writer(io::BufWriter::new(&mut &self.lock), &self.inner).map_err(|err| {
|
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.
|
/// Add a TOTP entry for a user. Returns the ID.
|
||||||
pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result<String, Error> {
|
pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result<String, Error> {
|
||||||
let _lock = write_lock();
|
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> {
|
pub fn add_u2f_registration(userid: &Userid, description: String) -> Result<String, Error> {
|
||||||
let _lock = crate::config::tfa::write_lock();
|
let _lock = crate::config::tfa::write_lock();
|
||||||
let mut data = read()?;
|
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)?;
|
write(&data)?;
|
||||||
Ok(challenge)
|
Ok(challenge)
|
||||||
}
|
}
|
||||||
@ -166,7 +158,7 @@ pub fn finish_u2f_registration(
|
|||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let _lock = crate::config::tfa::write_lock();
|
let _lock = crate::config::tfa::write_lock();
|
||||||
let mut data = read()?;
|
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)?;
|
write(&data)?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
@ -176,7 +168,7 @@ pub fn add_webauthn_registration(userid: &Userid, description: String) -> Result
|
|||||||
let _lock = crate::config::tfa::write_lock();
|
let _lock = crate::config::tfa::write_lock();
|
||||||
let mut data = read()?;
|
let mut data = read()?;
|
||||||
let challenge =
|
let challenge =
|
||||||
data.webauthn_registration_challenge(UserAccess, userid.as_str(), description, None)?;
|
data.webauthn_registration_challenge(&UserAccess, userid.as_str(), description, None)?;
|
||||||
write(&data)?;
|
write(&data)?;
|
||||||
Ok(challenge)
|
Ok(challenge)
|
||||||
}
|
}
|
||||||
@ -190,39 +182,20 @@ pub fn finish_webauthn_registration(
|
|||||||
let _lock = crate::config::tfa::write_lock();
|
let _lock = crate::config::tfa::write_lock();
|
||||||
let mut data = read()?;
|
let mut data = read()?;
|
||||||
let id =
|
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)?;
|
write(&data)?;
|
||||||
Ok(id)
|
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)]
|
#[derive(Clone, Copy)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct UserAccess;
|
pub struct UserAccess;
|
||||||
|
|
||||||
/// Build th
|
/// Build th
|
||||||
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
|
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
|
/// 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.
|
/// 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()?;
|
crate::server::create_run_dir()?;
|
||||||
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
|
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
|
||||||
proxmox_sys::fs::create_path(CHALLENGE_DATA_PATH, Some(options.clone()), Some(options))
|
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,
|
inner,
|
||||||
path,
|
path,
|
||||||
lock: file,
|
lock: file,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `open` without creating the file if it doesn't exist, to finish WA authentications.
|
/// `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 path = challenge_data_path_str(userid);
|
||||||
let mut file = match std::fs::OpenOptions::new()
|
let mut file = match std::fs::OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
@ -297,11 +270,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
|
|||||||
format_err!("failed to read challenge data for user {}: {}", userid, err)
|
format_err!("failed to read challenge data for user {}: {}", userid, err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Some(TfaUserChallengeData {
|
Ok(Some(Box::new(TfaUserChallengeData {
|
||||||
inner,
|
inner,
|
||||||
path,
|
path,
|
||||||
lock: file,
|
lock: file,
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `remove` user data if it exists.
|
/// `remove` user data if it exists.
|
||||||
@ -320,7 +293,7 @@ impl proxmox_tfa::api::UserChallengeAccess for TfaUserChallengeData {
|
|||||||
&mut self.inner
|
&mut self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(self) -> Result<(), Error> {
|
fn save(&mut self) -> Result<(), Error> {
|
||||||
TfaUserChallengeData::save(self)
|
TfaUserChallengeData::save(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ pub mod api2;
|
|||||||
|
|
||||||
pub mod auth_helpers;
|
pub mod auth_helpers;
|
||||||
|
|
||||||
pub mod auth;
|
pub(crate) mod auth;
|
||||||
|
|
||||||
pub mod tape;
|
pub mod tape;
|
||||||
|
|
||||||
|
@ -1,115 +1,13 @@
|
|||||||
//! Provides authentication primitives for the HTTP server
|
use proxmox_rest_server::AuthError;
|
||||||
|
|
||||||
use anyhow::format_err;
|
|
||||||
|
|
||||||
use proxmox_router::UserInformation;
|
use proxmox_router::UserInformation;
|
||||||
|
|
||||||
use pbs_api_types::{Authid, Userid};
|
use pbs_config::CachedUserInfo;
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_pbs_auth(
|
pub async fn check_pbs_auth(
|
||||||
headers: &http::HeaderMap,
|
headers: &http::HeaderMap,
|
||||||
method: &hyper::Method,
|
method: &hyper::Method,
|
||||||
) -> Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError> {
|
) -> Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError> {
|
||||||
// fixme: make all IO async
|
|
||||||
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
proxmox_auth_api::api::http_check_auth(headers, method)
|
||||||
let auth_data = extract_auth_data(headers);
|
.map(move |name| (name, Box::new(user_info) as _))
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,6 @@ pub use email_notifications::*;
|
|||||||
mod report;
|
mod report;
|
||||||
pub use report::*;
|
pub use report::*;
|
||||||
|
|
||||||
pub mod ticket;
|
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
|
||||||
pub(crate) mod pull;
|
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…
Reference in New Issue
Block a user