add proxmox-auth-api crate
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
a8bd8fca15
commit
5349ae208b
@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"proxmox-api-macro",
|
||||
"proxmox-async",
|
||||
"proxmox-auth-api",
|
||||
"proxmox-borrow",
|
||||
"proxmox-compression",
|
||||
"proxmox-http",
|
||||
@ -56,6 +57,8 @@ native-tls = "0.2"
|
||||
nix = "0.26.1"
|
||||
once_cell = "1.3.1"
|
||||
openssl = "0.10"
|
||||
pam = "0.7"
|
||||
pam-sys = "0.5"
|
||||
percent-encoding = "2.1"
|
||||
pin-utils = "0.1.0"
|
||||
proc-macro2 = "1.0"
|
||||
@ -82,10 +85,12 @@ proxmox-compression = { version = "0.1.1", path = "proxmox-compression" }
|
||||
proxmox-http = { version = "0.8.0", path = "proxmox-http" }
|
||||
proxmox-io = { version = "1.0.0", path = "proxmox-io" }
|
||||
proxmox-lang = { version = "1.1", path = "proxmox-lang" }
|
||||
proxmox-rest-server = { version = "0.3.0", path = "proxmox-rest-server" }
|
||||
proxmox-router = { version = "1.3.1", path = "proxmox-router" }
|
||||
proxmox-schema = { version = "1.3.6", path = "proxmox-schema" }
|
||||
proxmox-serde = { version = "0.1.1", path = "proxmox-serde", features = [ "serde_json" ] }
|
||||
proxmox-sortable-macro = { version = "0.1.2", path = "proxmox-sortable-macro" }
|
||||
proxmox-sys = { version = "0.4.2", path = "proxmox-sys" }
|
||||
proxmox-tfa = { version = "2.1.0", path = "proxmox-tfa" }
|
||||
proxmox-time = { version = "1.1.4", path = "proxmox-time" }
|
||||
proxmox-uuid = { version = "1.0.1", path = "proxmox-uuid" }
|
||||
|
49
proxmox-auth-api/Cargo.toml
Normal file
49
proxmox-auth-api/Cargo.toml
Normal file
@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "proxmox-auth-api"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
exclude.workspace = true
|
||||
description = "Tickets, API and Realm handling"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
base64 = { workspace = true, optional = true }
|
||||
lazy_static = { workspace = true, optional = true }
|
||||
libc = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
openssl = { workspace = true, optional = true }
|
||||
pam = { workspace = true, optional = true }
|
||||
pam-sys = { workspace = true, optional = true }
|
||||
percent-encoding = { workspace = true, optional = true }
|
||||
regex = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true, features = [ "derive" ] }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
serde_plain = { workspace = true, optional = true }
|
||||
|
||||
proxmox-rest-server = { workspace = true, optional = true }
|
||||
proxmox-router = { workspace = true, optional = true }
|
||||
proxmox-schema = { workspace = true, optional = true, features = [ "api-macro", "api-types" ] }
|
||||
proxmox-tfa = { workspace = true, optional = true, features = [ "api" ] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
ticket = [ "dep:base64", "dep:percent-encoding", "dep:openssl" ]
|
||||
api-types = [ "dep:lazy_static", "dep:regex", "dep:serde", "dep:serde_plain", "dep:proxmox-schema" ]
|
||||
api = [
|
||||
"api-types",
|
||||
"ticket",
|
||||
|
||||
"dep:http",
|
||||
"dep:serde_json",
|
||||
|
||||
"dep:proxmox-rest-server",
|
||||
"dep:proxmox-router",
|
||||
"dep:proxmox-tfa",
|
||||
]
|
||||
pam-authenticator = [ "api", "dep:libc", "dep:log", "dep:pam", "dep:pam-sys" ]
|
96
proxmox-auth-api/examples/passwd.rs
Normal file
96
proxmox-auth-api/examples/passwd.rs
Normal file
@ -0,0 +1,96 @@
|
||||
//! Test the `Pam` authenticator's 'store_password' implementation.
|
||||
|
||||
use std::future::Future;
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
use proxmox_auth_api::api::Authenticator;
|
||||
use proxmox_auth_api::types::Username;
|
||||
|
||||
static LOG: PrintLog = PrintLog;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
poll_result_once(run())
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
log::set_logger(&LOG).unwrap();
|
||||
log::set_max_level(log::LevelFilter::Debug);
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
let (username, changepass): (Username, bool) = match args.next() {
|
||||
None => bail!("missing username or --check parameter"),
|
||||
Some(ck) if ck == "--check" => (
|
||||
args.next()
|
||||
.ok_or_else(|| format_err!("expected username as paramter"))?
|
||||
.try_into()?,
|
||||
false,
|
||||
),
|
||||
Some(username) => (username.try_into()?, true),
|
||||
};
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(b"New password: ")?;
|
||||
stdout.flush()?;
|
||||
|
||||
let mut input = std::io::stdin().lines();
|
||||
let password = input
|
||||
.next()
|
||||
.ok_or_else(|| format_err!("failed to read new password"))??;
|
||||
|
||||
let realm = proxmox_auth_api::Pam::new("test");
|
||||
if changepass {
|
||||
realm.store_password(&username, &password)?;
|
||||
} else {
|
||||
realm.authenticate_user(&username, &password).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PrintLog;
|
||||
|
||||
impl log::Log for PrintLog {
|
||||
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record<'_>) {
|
||||
let _ = writeln!(std::io::stdout(), "{}", record.args());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_result_once<T, R>(mut fut: T) -> Result<R, Error>
|
||||
where
|
||||
T: Future<Output = Result<R, Error>>,
|
||||
{
|
||||
let waker = std::task::RawWaker::new(std::ptr::null(), &WAKER_VTABLE);
|
||||
let waker = unsafe { std::task::Waker::from_raw(waker) };
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
unsafe {
|
||||
match Pin::new_unchecked(&mut fut).poll(&mut cx) {
|
||||
Poll::Pending => bail!("got Poll::Pending synchronous context"),
|
||||
Poll::Ready(r) => r,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WAKER_VTABLE: std::task::RawWakerVTable =
|
||||
std::task::RawWakerVTable::new(forbid_clone, forbid_wake, forbid_wake, ignore_drop);
|
||||
|
||||
unsafe fn forbid_clone(_: *const ()) -> std::task::RawWaker {
|
||||
panic!("tried to clone waker for synchronous task");
|
||||
}
|
||||
|
||||
unsafe fn forbid_wake(_: *const ()) {
|
||||
panic!("tried to wake synchronous task");
|
||||
}
|
||||
|
||||
unsafe fn ignore_drop(_: *const ()) {}
|
298
proxmox-auth-api/src/api/access.rs
Normal file
298
proxmox-auth-api/src/api/access.rs
Normal file
@ -0,0 +1,298 @@
|
||||
//! Provides the "/access/ticket" API call.
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use proxmox_router::{http_err, Permission, RpcEnvironment};
|
||||
use proxmox_schema::{api, api_types::PASSWORD_SCHEMA};
|
||||
use proxmox_tfa::api::TfaChallenge;
|
||||
|
||||
use super::auth_context;
|
||||
use super::ApiTicket;
|
||||
use crate::ticket::Ticket;
|
||||
use crate::types::{Authid, Userid};
|
||||
|
||||
#[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>),
|
||||
}
|
||||
|
||||
#[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 auth_context = auth_context()?;
|
||||
let api_ticket = ApiTicket::Full(username.clone());
|
||||
let ticket = Ticket::new(auth_context.auth_prefix(), &api_ticket)?
|
||||
.sign(auth_context.keyring(), None)?;
|
||||
let token = assemble_csrf_prevention_token(auth_context.csrf_secret(), &username);
|
||||
|
||||
env.log_auth(username.as_str());
|
||||
|
||||
Ok(json!({
|
||||
"username": username,
|
||||
"ticket": ticket,
|
||||
"CSRFPreventionToken": token,
|
||||
}))
|
||||
}
|
||||
Ok(AuthResult::Partial(challenge)) => {
|
||||
let auth_context = auth_context()?;
|
||||
let api_ticket = ApiTicket::Partial(challenge);
|
||||
let ticket = Ticket::new(auth_context.auth_prefix(), &api_ticket)?
|
||||
.sign(auth_context.keyring(), 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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 auth_context = auth_context()?;
|
||||
let prefix = auth_context.auth_prefix();
|
||||
|
||||
let auth_id = Authid::from(userid.clone());
|
||||
if !auth_context.auth_id_is_active(&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(prefix) && password.as_bytes().get(prefix.len()).copied() == Some(b':')
|
||||
{
|
||||
if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
|
||||
.and_then(|ticket| ticket.verify(auth_context.keyring(), prefix, None))
|
||||
{
|
||||
if *userid == ticket_userid {
|
||||
return Ok(AuthResult::CreateTicket);
|
||||
}
|
||||
bail!("ticket login failed - wrong userid");
|
||||
}
|
||||
} else if let Some(((path, privs), port)) = path.zip(privs).zip(port) {
|
||||
match auth_context.check_path_ticket(userid, password, path, privs, port)? {
|
||||
None => (), // no path based tickets supported, just fall through.
|
||||
Some(true) => return Ok(AuthResult::Success),
|
||||
Some(false) => bail!("No such privilege"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::let_unit_value)]
|
||||
{
|
||||
let _: () = auth_context
|
||||
.lookup_realm(userid.realm())
|
||||
.ok_or_else(|| format_err!("unknown realm {:?}", userid.realm().as_str()))?
|
||||
.authenticate_user(userid.name(), password)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(match 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 auth_context = auth_context()?;
|
||||
let challenge: Box<TfaChallenge> = Ticket::<ApiTicket>::parse(challenge_ticket)?
|
||||
.verify_with_time_frame(
|
||||
auth_context.keyring(),
|
||||
auth_context.auth_prefix(),
|
||||
Some(userid.as_str()),
|
||||
-60..600,
|
||||
)?
|
||||
.require_partial()?;
|
||||
|
||||
#[allow(clippy::let_unit_value)]
|
||||
{
|
||||
let mut tfa_config_lock = auth_context.tfa_config_write_lock()?;
|
||||
let (locked_config, tfa_config) = tfa_config_lock.config_mut();
|
||||
if tfa_config
|
||||
.verify(
|
||||
locked_config,
|
||||
userid.as_str(),
|
||||
&challenge,
|
||||
response.parse()?,
|
||||
None,
|
||||
)?
|
||||
.needs_saving()
|
||||
{
|
||||
tfa_config_lock.save_config()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthResult::CreateTicket)
|
||||
}
|
||||
|
||||
fn login_challenge(userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
|
||||
let auth_context = auth_context()?;
|
||||
let mut tfa_config_lock = auth_context.tfa_config_write_lock()?;
|
||||
let (locked_config, tfa_config) = tfa_config_lock.config_mut();
|
||||
tfa_config.authentication_challenge(locked_config, userid.as_str(), None)
|
||||
}
|
||||
|
||||
fn assemble_csrf_prevention_token(secret: &[u8], userid: &Userid) -> String {
|
||||
let epoch = crate::time::epoch_i64();
|
||||
|
||||
let digest = compute_csrf_secret_digest(epoch, secret, userid);
|
||||
|
||||
format!("{:08X}:{}", epoch, digest)
|
||||
}
|
||||
|
||||
fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String {
|
||||
let mut hasher = openssl::sha::Sha256::new();
|
||||
let data = format!("{:08X}:{}:", timestamp, userid);
|
||||
hasher.update(data.as_bytes());
|
||||
hasher.update(secret);
|
||||
|
||||
base64::encode_config(hasher.finish(), base64::STANDARD_NO_PAD)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_csrf_prevention_token(
|
||||
secret: &[u8],
|
||||
userid: &Userid,
|
||||
token: &str,
|
||||
min_age: i64,
|
||||
max_age: i64,
|
||||
) -> Result<i64, Error> {
|
||||
verify_csrf_prevention_token_do(secret, userid, token, min_age, max_age)
|
||||
.map_err(|err| format_err!("invalid csrf token - {}", err))
|
||||
}
|
||||
|
||||
fn verify_csrf_prevention_token_do(
|
||||
secret: &[u8],
|
||||
userid: &Userid,
|
||||
token: &str,
|
||||
min_age: i64,
|
||||
max_age: i64,
|
||||
) -> Result<i64, Error> {
|
||||
use std::collections::VecDeque;
|
||||
|
||||
let mut parts: VecDeque<&str> = token.split(':').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
bail!("format error - wrong number of parts.");
|
||||
}
|
||||
|
||||
let timestamp = parts.pop_front().unwrap();
|
||||
let sig = parts.pop_front().unwrap();
|
||||
|
||||
let ttime = i64::from_str_radix(timestamp, 16)
|
||||
.map_err(|err| format_err!("timestamp format error - {}", err))?;
|
||||
|
||||
let digest = compute_csrf_secret_digest(ttime, secret, userid);
|
||||
|
||||
if digest != sig {
|
||||
bail!("invalid signature.");
|
||||
}
|
||||
|
||||
let now = crate::time::epoch_i64();
|
||||
|
||||
let age = now - ttime;
|
||||
if age < min_age {
|
||||
bail!("timestamp newer than expected.");
|
||||
}
|
||||
|
||||
if age > max_age {
|
||||
bail!("timestamp too old.");
|
||||
}
|
||||
|
||||
Ok(age)
|
||||
}
|
220
proxmox-auth-api/src/api/mod.rs
Normal file
220
proxmox-auth-api/src/api/mod.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{format_err, Error};
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
||||
use proxmox_rest_server::{extract_cookie, AuthError};
|
||||
use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
|
||||
|
||||
use crate::auth_key::Keyring;
|
||||
use crate::types::{Authid, RealmRef, Userid, UsernameRef};
|
||||
|
||||
mod access;
|
||||
mod ticket;
|
||||
|
||||
use crate::ticket::Ticket;
|
||||
use access::verify_csrf_prevention_token;
|
||||
|
||||
pub use access::{create_ticket, API_METHOD_CREATE_TICKET};
|
||||
pub use ticket::{ApiTicket, PartialTicket};
|
||||
|
||||
/// Authentication realms are used to manage users: authenticate, change password or remove.
|
||||
pub trait Authenticator {
|
||||
/// Authenticate a user given a password.
|
||||
fn authenticate_user<'a>(
|
||||
&'a self,
|
||||
username: &'a UsernameRef,
|
||||
password: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
|
||||
|
||||
/// Change a user's password.
|
||||
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
|
||||
|
||||
/// Remove a user.
|
||||
fn remove_password(&self, username: &UsernameRef) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// This provides access to the available realms and authentication keys.
|
||||
pub trait AuthContext: Send + Sync {
|
||||
/// Lookup a realm by name.
|
||||
fn lookup_realm(&self, realm: &RealmRef) -> Option<Box<dyn Authenticator + Send + Sync>>;
|
||||
|
||||
/// Get the current authentication keyring.
|
||||
fn keyring(&self) -> &Keyring;
|
||||
|
||||
/// The auth prefix without the separating colon. Eg. `"PBS"`.
|
||||
fn auth_prefix(&self) -> &'static str;
|
||||
|
||||
/// API token prefix (without the `'='`).
|
||||
fn auth_token_prefix(&self) -> &'static str;
|
||||
|
||||
/// Auth cookie name.
|
||||
fn auth_cookie_name(&self) -> &'static str;
|
||||
|
||||
/// Access the TFA config with an exclusive lock.
|
||||
fn tfa_config_write_lock(&self) -> Result<Box<dyn LockedTfaConfig>, Error>;
|
||||
|
||||
/// Check if a userid is enabled and return a [`UserInformation`] handle.
|
||||
fn auth_id_is_active(&self, auth_id: &Authid) -> Result<bool, Error>;
|
||||
|
||||
/// CSRF prevention token secret data.
|
||||
fn csrf_secret(&self) -> &[u8];
|
||||
|
||||
/// Verify a token secret.
|
||||
fn verify_token_secret(&self, token_id: &Authid, token_secret: &str) -> Result<(), Error>;
|
||||
|
||||
/// 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> {
|
||||
let _ = (userid, password, path, privs, port);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// When verifying TFA challenges we need to be able to update the TFA config without interference
|
||||
/// from other threads. Similarly, to authenticate with recovery keys, we need to be able to
|
||||
/// atomically mark them as used.
|
||||
pub trait LockedTfaConfig {
|
||||
/// Get mutable access to the [`TfaConfig`] and retain immutable access to `self`.
|
||||
fn config_mut(&mut self) -> (&dyn OpenUserChallengeData, &mut TfaConfig);
|
||||
|
||||
// Save the modified [`TfaConfig`].
|
||||
//
|
||||
// The config will have been modified by accessing the
|
||||
// [`config_mut`](LockedTfaConfig::config_mut()) method.
|
||||
fn save_config(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
static AUTH_CONTEXT: Mutex<Option<&'static dyn AuthContext>> = Mutex::new(None);
|
||||
|
||||
/// Configure access to authentication realms and keys.
|
||||
pub fn set_auth_context(auth_context: &'static dyn AuthContext) {
|
||||
*AUTH_CONTEXT.lock().unwrap() = Some(auth_context);
|
||||
}
|
||||
|
||||
fn auth_context() -> Result<&'static dyn AuthContext, Error> {
|
||||
AUTH_CONTEXT
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| format_err!("no realm access configured"))
|
||||
}
|
||||
|
||||
struct UserAuthData {
|
||||
ticket: String,
|
||||
csrf_token: Option<String>,
|
||||
}
|
||||
|
||||
enum AuthData {
|
||||
User(UserAuthData),
|
||||
ApiToken(String),
|
||||
}
|
||||
|
||||
pub fn http_check_auth(
|
||||
headers: &http::HeaderMap,
|
||||
method: &http::Method,
|
||||
) -> Result<String, AuthError> {
|
||||
let auth_context = auth_context()?;
|
||||
|
||||
let auth_data = extract_auth_data(auth_context, headers);
|
||||
match auth_data {
|
||||
Some(AuthData::User(user_auth_data)) => {
|
||||
let ticket = user_auth_data.ticket.clone();
|
||||
let ticket_lifetime = crate::TICKET_LIFETIME;
|
||||
|
||||
let userid: Userid = Ticket::<ApiTicket>::parse(&ticket)?
|
||||
.verify_with_time_frame(
|
||||
auth_context.keyring(),
|
||||
auth_context.auth_prefix(),
|
||||
None,
|
||||
-300..ticket_lifetime,
|
||||
)?
|
||||
.require_full()?;
|
||||
|
||||
let auth_id = Authid::from(userid.clone());
|
||||
if !auth_context.auth_id_is_active(&auth_id)? {
|
||||
return Err(format_err!("user account disabled or expired.").into());
|
||||
}
|
||||
|
||||
if method != http::Method::GET {
|
||||
if let Some(csrf_token) = &user_auth_data.csrf_token {
|
||||
verify_csrf_prevention_token(
|
||||
auth_context.csrf_secret(),
|
||||
&userid,
|
||||
csrf_token,
|
||||
-300,
|
||||
ticket_lifetime,
|
||||
)?;
|
||||
} else {
|
||||
return Err(format_err!("missing CSRF prevention token").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(auth_id.to_string())
|
||||
}
|
||||
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 !auth_context.auth_id_is_active(&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"))?;
|
||||
|
||||
auth_context.verify_token_secret(&tokenid, &tokensecret)?;
|
||||
|
||||
Ok(tokenid.to_string())
|
||||
}
|
||||
None => Err(AuthError::NoData),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_auth_data(
|
||||
auth_context: &dyn AuthContext,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Option<AuthData> {
|
||||
if let Some(raw_cookie) = headers.get(http::header::COOKIE) {
|
||||
if let Ok(cookie) = raw_cookie.to_str() {
|
||||
if let Some(ticket) = extract_cookie(cookie, auth_context.auth_cookie_name()) {
|
||||
let csrf_token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
|
||||
Some(Ok(v)) => Some(v.to_owned()),
|
||||
_ => None,
|
||||
};
|
||||
return Some(AuthData::User(UserAuthData { ticket, csrf_token }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let token_prefix = auth_context.auth_token_prefix();
|
||||
match headers.get(http::header::AUTHORIZATION).map(|v| v.to_str()) {
|
||||
Some(Ok(v)) => {
|
||||
if !v.starts_with(token_prefix) {
|
||||
return None;
|
||||
}
|
||||
match v.as_bytes().get(token_prefix.len()).copied() {
|
||||
Some(b' ') | Some(b'=') => {
|
||||
Some(AuthData::ApiToken(v[(token_prefix.len() + 1)..].to_owned()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
70
proxmox-auth-api/src/api/ticket.rs
Normal file
70
proxmox-auth-api/src/api/ticket.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! API side ticket utility.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use proxmox_tfa::api::TfaChallenge;
|
||||
|
||||
use crate::types::Userid;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PartialTicket {
|
||||
#[serde(rename = "u")]
|
||||
pub userid: Userid,
|
||||
|
||||
#[serde(rename = "c")]
|
||||
pub challenge: TfaChallenge,
|
||||
}
|
||||
|
||||
/// A new ticket struct used in `check_auth` - mostly for better errors than failing to parse the
|
||||
/// userid ticket content.
|
||||
pub enum ApiTicket {
|
||||
Full(Userid),
|
||||
Partial(Box<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<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()?))
|
||||
}
|
||||
}
|
||||
}
|
218
proxmox-auth-api/src/auth_key.rs
Normal file
218
proxmox-auth-api/src/auth_key.rs
Normal file
@ -0,0 +1,218 @@
|
||||
//! Auth key handling.
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use openssl::ec::{EcGroup, EcKey};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkey::{HasPublic, PKey, PKeyRef, Private, Public};
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::sign::{Signer, Verifier};
|
||||
|
||||
/// A private auth key used for API ticket signing and verification.
|
||||
pub struct PrivateKey {
|
||||
pub(crate) key: PKey<Private>,
|
||||
}
|
||||
|
||||
/// A private auth key used for API ticket verification.
|
||||
pub struct PublicKey {
|
||||
pub(crate) key: PKey<Public>,
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Generate a new RSA auth key.
|
||||
pub fn generate_rsa() -> Result<Self, Error> {
|
||||
let rsa =
|
||||
Rsa::generate(4096).map_err(|err| format_err!("failed to generate rsa key - {err}"))?;
|
||||
Ok(Self {
|
||||
key: PKey::from_rsa(rsa)
|
||||
.map_err(|err| format_err!("failed to get PKey for rsa key - {err}"))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a new EC auth key.
|
||||
pub fn generate_ec() -> Result<Self, Error> {
|
||||
let nid = Nid::X9_62_PRIME256V1;
|
||||
let group = EcGroup::from_curve_name(nid)
|
||||
.map_err(|err| format_err!("failed to get P-256 group - {err}"))?;
|
||||
let ec = EcKey::generate(&group)
|
||||
.map_err(|err| format_err!("failed to generate EC key for testing - {err}"))?;
|
||||
Ok(Self {
|
||||
key: PKey::from_ec_key(ec)
|
||||
.map_err(|err| format_err!("failed to get PKey for EC key - {err}"))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_pem(data: &[u8]) -> Result<Self, Error> {
|
||||
let key = PKey::private_key_from_pem(data)
|
||||
.map_err(|err| format_err!("failed to decode private key from PEM - {err}"))?;
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
/// Get the PEM formatted private key *unencrypted*.
|
||||
pub fn private_key_to_pem(&self) -> Result<Vec<u8>, Error> {
|
||||
// No PKCS#8 for legacy reasons:
|
||||
if let Ok(rsa) = self.key.rsa() {
|
||||
return rsa
|
||||
.private_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode rsa private key as PEM - {err}"));
|
||||
}
|
||||
|
||||
if let Ok(ec) = self.key.ec_key() {
|
||||
return ec
|
||||
.private_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode ec private key as PEM - {err}"));
|
||||
}
|
||||
|
||||
bail!("unexpected key data")
|
||||
}
|
||||
|
||||
/// Get the PEM formatted public key.
|
||||
pub fn public_key_to_pem(&self) -> Result<Vec<u8>, Error> {
|
||||
// No PKCS#8 for legacy reasons:
|
||||
if let Ok(rsa) = self.key.rsa() {
|
||||
return rsa
|
||||
.public_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode rsa public key as PEM - {err}"));
|
||||
}
|
||||
|
||||
if let Ok(ec) = self.key.ec_key() {
|
||||
return ec
|
||||
.public_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode ec public key as PEM - {err}"));
|
||||
}
|
||||
|
||||
bail!("unexpected key data")
|
||||
}
|
||||
|
||||
/// Get the public key.
|
||||
pub fn public_key(&self) -> Result<PublicKey, Error> {
|
||||
PublicKey::from_pem(&self.public_key_to_pem()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PKey<Private>> for PrivateKey {
|
||||
fn from(key: PKey<Private>) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn from_pem(data: &[u8]) -> Result<Self, Error> {
|
||||
let key = PKey::public_key_from_pem(data)
|
||||
.map_err(|err| format_err!("failed to decode public key from PEM - {err}"))?;
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
/// Get the PEM formatted public key.
|
||||
pub fn public_key_to_pem(&self) -> Result<Vec<u8>, Error> {
|
||||
// No PKCS#8 for legacy reasons:
|
||||
if let Ok(rsa) = self.key.rsa() {
|
||||
return rsa
|
||||
.public_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode rsa public key as PEM - {err}"));
|
||||
}
|
||||
|
||||
if let Ok(ec) = self.key.ec_key() {
|
||||
return ec
|
||||
.public_key_to_pem()
|
||||
.map_err(|err| format_err!("failed to encode ec public key as PEM - {err}"));
|
||||
}
|
||||
|
||||
bail!("unexpected key data")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PKey<Public>> for PublicKey {
|
||||
fn from(key: PKey<Public>) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
|
||||
/// A key ring for authentication.
|
||||
///
|
||||
/// This holds one active signing key for new tickets, and optionally multiple public keys for
|
||||
/// verifying them in order to support key rollover.
|
||||
pub struct Keyring {
|
||||
signing_key: Option<PrivateKey>,
|
||||
public_keys: Vec<PublicKey>,
|
||||
}
|
||||
|
||||
impl Keyring {
|
||||
pub fn generate_new_rsa() -> Result<Self, Error> {
|
||||
PrivateKey::generate_rsa().map(Self::with_private_key)
|
||||
}
|
||||
|
||||
pub fn generate_new_ec() -> Result<Self, Error> {
|
||||
PrivateKey::generate_ec().map(Self::with_private_key)
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signing_key: None,
|
||||
public_keys: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_public_key(key: PublicKey) -> Self {
|
||||
Self {
|
||||
signing_key: None,
|
||||
public_keys: vec![key],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_private_key(key: PrivateKey) -> Self {
|
||||
Self {
|
||||
signing_key: Some(key),
|
||||
public_keys: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_public_key(&mut self, key: PublicKey) {
|
||||
self.public_keys.push(key);
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
&self,
|
||||
digest: MessageDigest,
|
||||
signature: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<bool, Error> {
|
||||
fn verify_with<P: HasPublic>(
|
||||
key: &PKeyRef<P>,
|
||||
digest: MessageDigest,
|
||||
signature: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<bool, Error> {
|
||||
Verifier::new(digest, key)
|
||||
.map_err(|err| format_err!("failed to create openssl verifier - {err}"))?
|
||||
.verify_oneshot(signature, data)
|
||||
.map_err(|err| format_err!("openssl error verifying data - {err}"))
|
||||
}
|
||||
|
||||
if let Some(key) = &self.signing_key {
|
||||
if verify_with(&key.key, digest, signature, data)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
for key in &self.public_keys {
|
||||
if verify_with(&key.key, digest, signature, data)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn signer(&self, digest: MessageDigest) -> Result<Signer, Error> {
|
||||
Signer::new(
|
||||
digest,
|
||||
&self
|
||||
.signing_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| format_err!("no private key available for signing"))?
|
||||
.key,
|
||||
)
|
||||
.map_err(|err| format_err!("failed to create openssl signer - {err}"))
|
||||
}
|
||||
}
|
36
proxmox-auth-api/src/lib.rs
Normal file
36
proxmox-auth-api/src/lib.rs
Normal file
@ -0,0 +1,36 @@
|
||||
//! Authentication API crate.
|
||||
//!
|
||||
//! This contains the API types for `Userid`/`Realm`/`Authid` etc., the PAM authenticator and the
|
||||
//! authentication API calls.
|
||||
//!
|
||||
//! Each can be enabled via a feature:
|
||||
//!
|
||||
//! The `pam-authenticator` feature enables the `Pam` type.
|
||||
|
||||
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
|
||||
|
||||
#[cfg(feature = "ticket")]
|
||||
mod time;
|
||||
|
||||
#[cfg(feature = "api")]
|
||||
pub mod api;
|
||||
|
||||
#[cfg(feature = "api")]
|
||||
pub use api::set_auth_context;
|
||||
|
||||
#[cfg(any(feature = "api", feature = "ticket"))]
|
||||
mod auth_key;
|
||||
|
||||
#[cfg(any(feature = "api", feature = "ticket"))]
|
||||
pub use auth_key::{Keyring, PrivateKey, PublicKey};
|
||||
|
||||
#[cfg(feature = "ticket")]
|
||||
pub mod ticket;
|
||||
|
||||
#[cfg(feature = "api-types")]
|
||||
pub mod types;
|
||||
|
||||
#[cfg(feature = "pam-authenticator")]
|
||||
mod pam_authenticator;
|
||||
#[cfg(feature = "pam-authenticator")]
|
||||
pub use pam_authenticator::Pam;
|
193
proxmox-auth-api/src/pam_authenticator.rs
Normal file
193
proxmox-auth-api/src/pam_authenticator.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::ffi::{c_int, c_void, CStr};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use pam_sys::types::{PamHandle, PamMessage, PamMessageStyle, PamResponse, PamReturnCode};
|
||||
|
||||
use crate::types::UsernameRef;
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct Pam {
|
||||
service: &'static str,
|
||||
}
|
||||
|
||||
impl Pam {
|
||||
pub const fn new(service: &'static str) -> Self {
|
||||
Self { service }
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::api::Authenticator for Pam {
|
||||
fn authenticate_user<'a>(
|
||||
&'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(self.service).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 password_conv = PasswordConv {
|
||||
login: username.as_str(),
|
||||
password,
|
||||
};
|
||||
|
||||
let conv = pam_sys::types::PamConversation {
|
||||
conv: Some(conv_fn),
|
||||
data_ptr: &mut password_conv as *mut _ as *mut c_void,
|
||||
};
|
||||
|
||||
let mut handle = std::ptr::null_mut();
|
||||
let err =
|
||||
pam_sys::wrapped::start(self.service, Some(username.as_str()), &conv, &mut handle);
|
||||
if err != PamReturnCode::SUCCESS {
|
||||
bail!("error opening pam - {err}");
|
||||
}
|
||||
let mut handle = PamGuard {
|
||||
handle: unsafe { &mut *handle },
|
||||
result: PamReturnCode::SUCCESS,
|
||||
};
|
||||
|
||||
/*
|
||||
* we assume we're root and don't need to authenticate
|
||||
handle.result =
|
||||
pam_sys::wrapped::authenticate(handle.handle, pam_sys::types::PamFlag::NONE);
|
||||
if handle.result != PamReturnCode::SUCCESS {
|
||||
bail!("authentication error - {err}");
|
||||
}
|
||||
|
||||
handle.result = pam_sys::wrapped::acct_mgmt(handle.handle, pam_sys::types::PamFlag::NONE);
|
||||
if handle.result != PamReturnCode::SUCCESS {
|
||||
bail!("account error - {}", handle.result);
|
||||
}
|
||||
*/
|
||||
|
||||
handle.result = pam_sys::wrapped::chauthtok(handle.handle, pam_sys::types::PamFlag::NONE);
|
||||
if handle.result != PamReturnCode::SUCCESS {
|
||||
bail!("error changing auth token - {}", handle.result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// do not remove password for pam users
|
||||
fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn conv_fn(
|
||||
num_messages: c_int,
|
||||
messages: *mut *mut PamMessage,
|
||||
responses_out: *mut *mut PamResponse,
|
||||
data_ptr: *mut c_void,
|
||||
) -> c_int {
|
||||
let messages: &[&PamMessage] = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
messages as *const *const PamMessage as *const &PamMessage,
|
||||
num_messages as usize,
|
||||
)
|
||||
};
|
||||
|
||||
let mut responses = Vec::new();
|
||||
responses.resize(
|
||||
messages.len(),
|
||||
PamResponse {
|
||||
resp: std::ptr::null_mut(),
|
||||
resp_retcode: 0,
|
||||
},
|
||||
);
|
||||
let mut responses = responses.into_boxed_slice();
|
||||
|
||||
let data_ptr = unsafe { &*(data_ptr as *const PasswordConv<'_>) };
|
||||
|
||||
match data_ptr.converse(messages, &mut responses) {
|
||||
Ok(()) => {
|
||||
unsafe {
|
||||
std::ptr::write(responses_out, &mut Box::leak(responses)[0]);
|
||||
}
|
||||
PamReturnCode::SUCCESS as c_int
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error conversing with pam - {err}");
|
||||
PamReturnCode::ABORT as c_int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PamGuard<'a> {
|
||||
handle: &'a mut PamHandle,
|
||||
result: PamReturnCode,
|
||||
}
|
||||
|
||||
impl Drop for PamGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
pam_sys::wrapped::end(&mut self.handle, self.result);
|
||||
}
|
||||
}
|
||||
|
||||
struct PasswordConv<'a> {
|
||||
login: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
|
||||
impl PasswordConv<'_> {
|
||||
fn converse(
|
||||
&self,
|
||||
messages: &[&PamMessage],
|
||||
responses: &mut [PamResponse],
|
||||
) -> Result<(), Error> {
|
||||
for i in 0..messages.len() {
|
||||
self.msg(messages[i], &mut responses[i])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn msg(
|
||||
&self,
|
||||
msg: &pam_sys::types::PamMessage,
|
||||
response: &mut PamResponse,
|
||||
) -> Result<(), Error> {
|
||||
let resp = match PamMessageStyle::from(msg.msg_style) {
|
||||
PamMessageStyle::PROMPT_ECHO_ON => {
|
||||
//let msg = unsafe { CStr::from_ptr(msg.msg) };
|
||||
//log::info!("pam prompt: {msg:?}");
|
||||
self.login
|
||||
}
|
||||
PamMessageStyle::PROMPT_ECHO_OFF => {
|
||||
//let msg = unsafe { CStr::from_ptr(msg.msg) };
|
||||
//log::info!("pam password prompt: {msg:?}");
|
||||
self.password
|
||||
}
|
||||
PamMessageStyle::ERROR_MSG => {
|
||||
let msg = unsafe { CStr::from_ptr(msg.msg) };
|
||||
log::error!("pam error: {msg:?}");
|
||||
return Ok(());
|
||||
}
|
||||
PamMessageStyle::TEXT_INFO => {
|
||||
let msg = unsafe { CStr::from_ptr(msg.msg) };
|
||||
log::info!("pam message: {msg:?}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Since CString::into_raw is technically not `free()`-safe...
|
||||
let resp = resp.as_bytes();
|
||||
let c_resp = unsafe { libc::malloc(resp.len() + 1) as *mut u8 };
|
||||
if c_resp.is_null() {
|
||||
bail!("failed to allocate response");
|
||||
}
|
||||
let c_resp = unsafe { std::slice::from_raw_parts_mut(c_resp, resp.len() + 1) };
|
||||
c_resp[c_resp.len() - 1] = 0;
|
||||
c_resp[..resp.len()].copy_from_slice(resp);
|
||||
response.resp = c_resp.as_mut_ptr() as *mut libc::c_char;
|
||||
Ok(())
|
||||
}
|
||||
}
|
334
proxmox-auth-api/src/ticket.rs
Normal file
334
proxmox-auth-api/src/ticket.rs
Normal file
@ -0,0 +1,334 @@
|
||||
//! Generate and verify Authentication tickets
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use openssl::hash::MessageDigest;
|
||||
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
|
||||
|
||||
use crate::auth_key::Keyring;
|
||||
|
||||
use crate::TICKET_LIFETIME;
|
||||
|
||||
/// 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: crate::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 string.
|
||||
fn ticket_data(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{:08X}",
|
||||
percent_encode(self.prefix.as_bytes(), TICKET_ASCIISET),
|
||||
percent_encode(self.data.as_bytes(), TICKET_ASCIISET),
|
||||
self.time,
|
||||
)
|
||||
}
|
||||
|
||||
/// Serialize the verification data.
|
||||
fn verification_data(&self, aad: Option<&str>) -> Vec<u8> {
|
||||
let mut data = self.ticket_data().into_bytes();
|
||||
if let Some(aad) = aad {
|
||||
data.push(b':');
|
||||
data.extend(aad.as_bytes());
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// 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, keyring: &Keyring, aad: Option<&str>) -> Result<String, Error> {
|
||||
let mut output = self.ticket_data();
|
||||
let mut signer = keyring.signer(MessageDigest::sha256())?;
|
||||
|
||||
signer
|
||||
.update(output.as_bytes())
|
||||
.map_err(Error::from)
|
||||
.and_then(|()| {
|
||||
if let Some(aad) = aad {
|
||||
signer
|
||||
.update(b":")
|
||||
.and_then(|()| signer.update(aad.as_bytes()))
|
||||
.map_err(Error::from)
|
||||
} else {
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
})
|
||||
.map_err(|err| format_err!("error signing ticket: {}", err))?;
|
||||
|
||||
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(
|
||||
&self,
|
||||
keyring: &Keyring,
|
||||
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_deref() {
|
||||
Some(sig) => sig,
|
||||
None => bail!("invalid ticket without signature"),
|
||||
};
|
||||
|
||||
let age = crate::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 is_valid = keyring.verify(
|
||||
MessageDigest::sha256(),
|
||||
&signature,
|
||||
&self.verification_data(aad),
|
||||
)?;
|
||||
|
||||
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(&self, keyring: &Keyring, prefix: &str, aad: Option<&str>) -> Result<T, Error> {
|
||||
self.verify_with_time_frame(keyring, 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 crate::auth_key::Keyring;
|
||||
|
||||
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>(keyring: &Keyring, 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(keyring, 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(keyring, "PREFIX", aad)
|
||||
.expect("failed to verify test ticket");
|
||||
|
||||
assert_eq!(userid, check);
|
||||
} else {
|
||||
parsed
|
||||
.verify(keyring, "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 keyring = Keyring::generate_new_rsa().expect("failed to generate RSA key for testing");
|
||||
|
||||
simple_test(&keyring, Some("secret aad data"), |_| true);
|
||||
simple_test(&keyring, None, |_| true);
|
||||
simple_test(&keyring, None, |t| {
|
||||
t.change_time(0);
|
||||
false
|
||||
});
|
||||
simple_test(&keyring, None, |t| {
|
||||
t.change_time(crate::time::epoch_i64() + 0x1000_0000);
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tickets_ecdsa() {
|
||||
let keyring = Keyring::generate_new_ec().expect("failed to generate EC key for testing");
|
||||
|
||||
simple_test(&keyring, Some("secret aad data"), |_| true);
|
||||
simple_test(&keyring, None, |_| true);
|
||||
simple_test(&keyring, None, |t| {
|
||||
t.change_time(0);
|
||||
false
|
||||
});
|
||||
simple_test(&keyring, None, |t| {
|
||||
t.change_time(crate::time::epoch_i64() + 0x1000_0000);
|
||||
false
|
||||
});
|
||||
}
|
||||
}
|
12
proxmox-auth-api/src/time.rs
Normal file
12
proxmox-auth-api/src/time.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Unix epoch.
|
||||
pub fn epoch_i64() -> i64 {
|
||||
let now = SystemTime::now();
|
||||
|
||||
if now > UNIX_EPOCH {
|
||||
i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs()).expect("epoch > 64 bit")
|
||||
} else {
|
||||
-i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs()).expect("epoch > 64 bit")
|
||||
}
|
||||
}
|
724
proxmox-auth-api/src/types.rs
Normal file
724
proxmox-auth-api/src/types.rs
Normal file
@ -0,0 +1,724 @@
|
||||
//! Types for user handling.
|
||||
//!
|
||||
//! We have [`Username`]s, [`Realm`]s and [`Tokenname`]s. To uniquely identify a user/API token, they
|
||||
//! must be combined into a [`Userid`] or [`Authid`].
|
||||
//!
|
||||
//! Since they're all string types, they're organized as follows:
|
||||
//!
|
||||
//! * [`Username`]: an owned user name. Internally a `String`.
|
||||
//! * [`UsernameRef`]: a borrowed user name. Pairs with a `Username` the same way a `str` pairs
|
||||
//! with `String`, meaning you can only make references to it.
|
||||
//! * [`Realm`]: an owned realm (`String` equivalent).
|
||||
//! * [`RealmRef`]: a borrowed realm (`str` equivalent).
|
||||
//! * [`Tokenname`]: an owned API token name (`String` equivalent)
|
||||
//! * [`TokennameRef`]: a borrowed `Tokenname` (`str` equivalent).
|
||||
//! * [`Userid`]: an owned user id (`"user@realm"`).
|
||||
//! * [`Authid`]: an owned Authentication ID (a `Userid` with an optional `Tokenname`).
|
||||
//! Note that `Userid` and `Authid` do not have a separate borrowed type.
|
||||
//!
|
||||
//! Note that `Username`s are not unique, therefore they do not implement `Eq` and cannot be
|
||||
//! compared directly. If a direct comparison is really required, they can be compared as strings
|
||||
//! via the `as_str()` method. [`Realm`]s, [`Userid`]s and [`Authid`]s on the other hand can be
|
||||
//! compared with each other, as in those cases the comparison has meaning.
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use proxmox_schema::{
|
||||
api, const_regex, ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType,
|
||||
};
|
||||
|
||||
// we only allow a limited set of characters
|
||||
// colon is not allowed, because we store usernames in
|
||||
// colon separated lists)!
|
||||
// slash is not allowed because it is used as pve API delimiter
|
||||
// also see "man useradd"
|
||||
#[macro_export]
|
||||
macro_rules! USER_NAME_REGEX_STR {
|
||||
() => {
|
||||
r"(?:[^\s:/[:cntrl:]]+)"
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! GROUP_NAME_REGEX_STR {
|
||||
() => {
|
||||
$crate::USER_NAME_REGEX_STR!()
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! TOKEN_NAME_REGEX_STR {
|
||||
() => {
|
||||
proxmox_schema::SAFE_ID_REGEX_STR!()
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! USER_ID_REGEX_STR {
|
||||
() => {
|
||||
concat!(
|
||||
$crate::USER_NAME_REGEX_STR!(),
|
||||
r"@",
|
||||
proxmox_schema::SAFE_ID_REGEX_STR!()
|
||||
)
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! APITOKEN_ID_REGEX_STR {
|
||||
() => {
|
||||
concat!(
|
||||
$crate::USER_ID_REGEX_STR!(),
|
||||
r"!",
|
||||
$crate::TOKEN_NAME_REGEX_STR!()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const_regex! {
|
||||
pub PROXMOX_USER_NAME_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"$");
|
||||
pub PROXMOX_TOKEN_NAME_REGEX = concat!(r"^", TOKEN_NAME_REGEX_STR!(), r"$");
|
||||
pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$");
|
||||
pub PROXMOX_APITOKEN_ID_REGEX = concat!(r"^", APITOKEN_ID_REGEX_STR!(), r"$");
|
||||
pub PROXMOX_AUTH_ID_REGEX = concat!(r"^", r"(?:", USER_ID_REGEX_STR!(), r"|", APITOKEN_ID_REGEX_STR!(), r")$");
|
||||
pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_REGEX_STR!(), r"$");
|
||||
}
|
||||
|
||||
pub const PROXMOX_USER_NAME_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_USER_NAME_REGEX);
|
||||
pub const PROXMOX_TOKEN_NAME_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_TOKEN_NAME_REGEX);
|
||||
|
||||
pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
|
||||
pub const PROXMOX_TOKEN_ID_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_APITOKEN_ID_REGEX);
|
||||
pub const PROXMOX_AUTH_ID_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_AUTH_ID_REGEX);
|
||||
|
||||
pub const PROXMOX_TOKEN_ID_SCHEMA: Schema = StringSchema::new("API Token ID")
|
||||
.format(&PROXMOX_TOKEN_ID_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(64)
|
||||
.schema();
|
||||
|
||||
pub const PROXMOX_TOKEN_NAME_SCHEMA: Schema = StringSchema::new("API Token name")
|
||||
.format(&PROXMOX_TOKEN_NAME_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(64)
|
||||
.schema();
|
||||
|
||||
pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
|
||||
ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
|
||||
|
||||
pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID")
|
||||
.format(&PROXMOX_GROUP_ID_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(64)
|
||||
.schema();
|
||||
|
||||
pub const PROXMOX_AUTH_REALM_STRING_SCHEMA: StringSchema =
|
||||
StringSchema::new("Authentication domain ID")
|
||||
.format(&proxmox_schema::api_types::SAFE_ID_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(32);
|
||||
pub const PROXMOX_AUTH_REALM_SCHEMA: Schema = PROXMOX_AUTH_REALM_STRING_SCHEMA.schema();
|
||||
|
||||
#[api(
|
||||
type: String,
|
||||
format: &PROXMOX_USER_NAME_FORMAT,
|
||||
min_length: 1,
|
||||
)]
|
||||
/// The user name part of a user id.
|
||||
///
|
||||
/// This alone does NOT uniquely identify the user and therefore does not implement `Eq`. In order
|
||||
/// to compare user names directly, they need to be explicitly compared as strings by calling
|
||||
/// `.as_str()`.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// fn test(a: Username, b: Username) -> bool {
|
||||
/// a == b // illegal and does not compile
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Hash, Deserialize, Serialize)]
|
||||
pub struct Username(String);
|
||||
|
||||
/// A reference to a user name part of a user id. This alone does NOT uniquely identify the user.
|
||||
///
|
||||
/// This is like a `str` to the `String` of a [`Username`].
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct UsernameRef(str);
|
||||
|
||||
impl UsernameRef {
|
||||
fn new(s: &str) -> &Self {
|
||||
unsafe { &*(s as *const str as *const UsernameRef) }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Username {
|
||||
type Target = UsernameRef;
|
||||
|
||||
fn deref(&self) -> &UsernameRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<UsernameRef> for Username {
|
||||
fn borrow(&self) -> &UsernameRef {
|
||||
UsernameRef::new(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<UsernameRef> for Username {
|
||||
fn as_ref(&self) -> &UsernameRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for UsernameRef {
|
||||
type Owned = Username;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
Username(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Username {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Error> {
|
||||
if !PROXMOX_USER_NAME_REGEX.is_match(&s) {
|
||||
bail!("invalid user name");
|
||||
}
|
||||
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a UsernameRef {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<&'a UsernameRef, Error> {
|
||||
if !PROXMOX_USER_NAME_REGEX.is_match(s) {
|
||||
bail!("invalid name in user id");
|
||||
}
|
||||
|
||||
Ok(UsernameRef::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[api(schema: PROXMOX_AUTH_REALM_SCHEMA)]
|
||||
/// An authentication realm.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
|
||||
pub struct Realm(String);
|
||||
|
||||
/// A reference to an authentication realm.
|
||||
///
|
||||
/// This is like a `str` to the `String` of a `Realm`.
|
||||
#[derive(Debug, Hash, Eq, PartialEq)]
|
||||
pub struct RealmRef(str);
|
||||
|
||||
impl RealmRef {
|
||||
fn new(s: &str) -> &Self {
|
||||
unsafe { &*(s as *const str as *const RealmRef) }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Realm {
|
||||
type Target = RealmRef;
|
||||
|
||||
fn deref(&self) -> &RealmRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<RealmRef> for Realm {
|
||||
fn borrow(&self) -> &RealmRef {
|
||||
RealmRef::new(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RealmRef> for Realm {
|
||||
fn as_ref(&self) -> &RealmRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for RealmRef {
|
||||
type Owned = Realm;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
Realm(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Realm {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Error> {
|
||||
PROXMOX_AUTH_REALM_STRING_SCHEMA
|
||||
.check_constraints(&s)
|
||||
.map_err(|_| format_err!("invalid realm"))?;
|
||||
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a RealmRef {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<&'a RealmRef, Error> {
|
||||
PROXMOX_AUTH_REALM_STRING_SCHEMA
|
||||
.check_constraints(s)
|
||||
.map_err(|_| format_err!("invalid realm"))?;
|
||||
|
||||
Ok(RealmRef::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for Realm {
|
||||
fn eq(&self, rhs: &str) -> bool {
|
||||
self.0 == rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for Realm {
|
||||
fn eq(&self, rhs: &&str) -> bool {
|
||||
self.0 == *rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for RealmRef {
|
||||
fn eq(&self, rhs: &str) -> bool {
|
||||
self.0 == *rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for RealmRef {
|
||||
fn eq(&self, rhs: &&str) -> bool {
|
||||
self.0 == **rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<RealmRef> for Realm {
|
||||
fn eq(&self, rhs: &RealmRef) -> bool {
|
||||
self.0 == rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Realm> for RealmRef {
|
||||
fn eq(&self, rhs: &Realm) -> bool {
|
||||
self.0 == rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Realm> for &RealmRef {
|
||||
fn eq(&self, rhs: &Realm) -> bool {
|
||||
self.0 == rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
#[api(
|
||||
type: String,
|
||||
format: &PROXMOX_TOKEN_NAME_FORMAT,
|
||||
)]
|
||||
/// The token ID part of an API token authentication id.
|
||||
///
|
||||
/// This alone does NOT uniquely identify the API token - use a full `Authid` for such use cases.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Tokenname(String);
|
||||
|
||||
/// A reference to a token name part of an authentication id. This alone does NOT uniquely identify
|
||||
/// the user.
|
||||
///
|
||||
/// This is like a `str` to the `String` of a [`Tokenname`].
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct TokennameRef(str);
|
||||
|
||||
#[doc(hidden)]
|
||||
/// ```compile_fail
|
||||
/// let a: Username = unsafe { std::mem::zeroed() };
|
||||
/// let b: Username = unsafe { std::mem::zeroed() };
|
||||
/// let _ = <Username as PartialEq>::eq(&a, &b);
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
|
||||
/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
|
||||
/// let _ = <&UsernameRef as PartialEq>::eq(a, b);
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// let a: &UsernameRef = unsafe { std::mem::zeroed() };
|
||||
/// let b: &UsernameRef = unsafe { std::mem::zeroed() };
|
||||
/// let _ = <&UsernameRef as PartialEq>::eq(&a, &b);
|
||||
/// ```
|
||||
struct _AssertNoEqImpl;
|
||||
|
||||
impl TokennameRef {
|
||||
fn new(s: &str) -> &Self {
|
||||
unsafe { &*(s as *const str as *const TokennameRef) }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Tokenname {
|
||||
type Target = TokennameRef;
|
||||
|
||||
fn deref(&self) -> &TokennameRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<TokennameRef> for Tokenname {
|
||||
fn borrow(&self) -> &TokennameRef {
|
||||
TokennameRef::new(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<TokennameRef> for Tokenname {
|
||||
fn as_ref(&self) -> &TokennameRef {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for TokennameRef {
|
||||
type Owned = Tokenname;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
Tokenname(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Tokenname {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Error> {
|
||||
if !PROXMOX_TOKEN_NAME_REGEX.is_match(&s) {
|
||||
bail!("invalid token name");
|
||||
}
|
||||
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a TokennameRef {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<&'a TokennameRef, Error> {
|
||||
if !PROXMOX_TOKEN_NAME_REGEX.is_match(s) {
|
||||
bail!("invalid token name in user id");
|
||||
}
|
||||
|
||||
Ok(TokennameRef::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete user id consisting of a user name and a realm
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, UpdaterType)]
|
||||
pub struct Userid {
|
||||
data: String,
|
||||
name_len: usize,
|
||||
}
|
||||
|
||||
impl ApiType for Userid {
|
||||
const API_SCHEMA: Schema = StringSchema::new("User ID")
|
||||
.format(&PROXMOX_USER_ID_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(64)
|
||||
.schema();
|
||||
}
|
||||
|
||||
impl Userid {
|
||||
const fn new(data: String, name_len: usize) -> Self {
|
||||
Self { data, name_len }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UsernameRef {
|
||||
UsernameRef::new(&self.data[..self.name_len])
|
||||
}
|
||||
|
||||
pub fn realm(&self) -> &RealmRef {
|
||||
RealmRef::new(&self.data[(self.name_len + 1)..])
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Get the "root@pam" user id.
|
||||
pub fn root_userid() -> &'static Self {
|
||||
&ROOT_USERID
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4);
|
||||
}
|
||||
|
||||
impl From<Authid> for Userid {
|
||||
fn from(authid: Authid) -> Self {
|
||||
authid.user
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Username, Realm)> for Userid {
|
||||
fn from(parts: (Username, Realm)) -> Self {
|
||||
Self::from((parts.0.as_ref(), parts.1.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&UsernameRef, &RealmRef)> for Userid {
|
||||
fn from(parts: (&UsernameRef, &RealmRef)) -> Self {
|
||||
let data = format!("{}@{}", parts.0.as_str(), parts.1.as_str());
|
||||
let name_len = parts.0.as_str().len();
|
||||
Self { data, name_len }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Userid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.data.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Userid {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(id: &str) -> Result<Self, Error> {
|
||||
let name_len = id
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'@')
|
||||
.ok_or_else(|| format_err!("not a valid user id"))?;
|
||||
|
||||
let name = &id[..name_len];
|
||||
let realm = &id[(name_len + 1)..];
|
||||
|
||||
if !PROXMOX_USER_NAME_REGEX.is_match(name) {
|
||||
bail!("invalid user name in user id");
|
||||
}
|
||||
|
||||
PROXMOX_AUTH_REALM_STRING_SCHEMA
|
||||
.check_constraints(realm)
|
||||
.map_err(|_| format_err!("invalid realm in user id"))?;
|
||||
|
||||
Ok(Self::from((UsernameRef::new(name), RealmRef::new(realm))))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Userid {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(data: String) -> Result<Self, Error> {
|
||||
let name_len = data
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'@')
|
||||
.ok_or_else(|| format_err!("not a valid user id"))?;
|
||||
|
||||
if !PROXMOX_USER_NAME_REGEX.is_match(&data[..name_len]) {
|
||||
bail!("invalid user name in user id");
|
||||
}
|
||||
|
||||
PROXMOX_AUTH_REALM_STRING_SCHEMA
|
||||
.check_constraints(&data[(name_len + 1)..])
|
||||
.map_err(|_| format_err!("invalid realm in user id"))?;
|
||||
|
||||
Ok(Self { data, name_len })
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for Userid {
|
||||
fn eq(&self, rhs: &str) -> bool {
|
||||
self.data == *rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for Userid {
|
||||
fn eq(&self, rhs: &&str) -> bool {
|
||||
*self == **rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<String> for Userid {
|
||||
fn eq(&self, rhs: &String) -> bool {
|
||||
self == rhs.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete authentication id consisting of a user id and an optional token name.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, UpdaterType, Ord, PartialOrd)]
|
||||
pub struct Authid {
|
||||
user: Userid,
|
||||
tokenname: Option<Tokenname>,
|
||||
}
|
||||
|
||||
impl ApiType for Authid {
|
||||
const API_SCHEMA: Schema = StringSchema::new("Authentication ID")
|
||||
.format(&PROXMOX_AUTH_ID_FORMAT)
|
||||
.min_length(3)
|
||||
.max_length(64)
|
||||
.schema();
|
||||
}
|
||||
|
||||
impl Authid {
|
||||
const fn new(user: Userid, tokenname: Option<Tokenname>) -> Self {
|
||||
Self { user, tokenname }
|
||||
}
|
||||
|
||||
pub fn user(&self) -> &Userid {
|
||||
&self.user
|
||||
}
|
||||
|
||||
pub fn is_token(&self) -> bool {
|
||||
self.tokenname.is_some()
|
||||
}
|
||||
|
||||
pub fn tokenname(&self) -> Option<&TokennameRef> {
|
||||
self.tokenname.as_deref()
|
||||
}
|
||||
|
||||
/// Get the "root@pam" auth id.
|
||||
pub fn root_auth_id() -> &'static Self {
|
||||
&ROOT_AUTHID
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root@pam".to_string(), 4));
|
||||
}
|
||||
|
||||
impl From<Userid> for Authid {
|
||||
fn from(parts: Userid) -> Self {
|
||||
Self::new(parts, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Userid, Option<Tokenname>)> for Authid {
|
||||
fn from(parts: (Userid, Option<Tokenname>)) -> Self {
|
||||
Self::new(parts.0, parts.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Authid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self.tokenname {
|
||||
Some(token) => write!(f, "{}!{}", self.user, token.as_str()),
|
||||
None => self.user.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Authid {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(id: &str) -> Result<Self, Error> {
|
||||
let name_len = id
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'@')
|
||||
.ok_or_else(|| format_err!("not a valid user id"))?;
|
||||
|
||||
let realm_end = id
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'!')
|
||||
.map(|pos| if pos < name_len { id.len() } else { pos })
|
||||
.unwrap_or_else(|| id.len());
|
||||
|
||||
if realm_end == id.len() - 1 {
|
||||
bail!("empty token name in userid");
|
||||
}
|
||||
|
||||
let user = Userid::from_str(&id[..realm_end])?;
|
||||
|
||||
if id.len() > realm_end {
|
||||
let token = Tokenname::try_from(id[(realm_end + 1)..].to_string())?;
|
||||
Ok(Self::new(user, Some(token)))
|
||||
} else {
|
||||
Ok(Self::new(user, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Authid {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(mut data: String) -> Result<Self, Error> {
|
||||
let name_len = data
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'@')
|
||||
.ok_or_else(|| format_err!("not a valid user id"))?;
|
||||
|
||||
let realm_end = data
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|&b| b == b'!')
|
||||
.map(|pos| if pos < name_len { data.len() } else { pos })
|
||||
.unwrap_or_else(|| data.len());
|
||||
|
||||
if realm_end == data.len() - 1 {
|
||||
bail!("empty token name in userid");
|
||||
}
|
||||
|
||||
let tokenname = if data.len() > realm_end {
|
||||
Some(Tokenname::try_from(data[(realm_end + 1)..].to_string())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
data.truncate(realm_end);
|
||||
|
||||
let user: Userid = data.parse()?;
|
||||
|
||||
Ok(Self { user, tokenname })
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_id() {
|
||||
let userid: Userid = "test@pam".parse().expect("parsing Userid failed");
|
||||
assert_eq!(userid.name().as_str(), "test");
|
||||
assert_eq!(userid.realm(), "pam");
|
||||
assert_eq!(userid, "test@pam");
|
||||
|
||||
let auth_id: Authid = "test@pam".parse().expect("parsing user Authid failed");
|
||||
assert_eq!(auth_id.to_string(), "test@pam".to_string());
|
||||
assert!(!auth_id.is_token());
|
||||
|
||||
assert_eq!(auth_id.user(), &userid);
|
||||
|
||||
let user_auth_id = Authid::from(userid.clone());
|
||||
assert_eq!(user_auth_id, auth_id);
|
||||
assert!(!user_auth_id.is_token());
|
||||
|
||||
let auth_id: Authid = "test@pam!bar".parse().expect("parsing token Authid failed");
|
||||
let token_userid = auth_id.user();
|
||||
assert_eq!(&userid, token_userid);
|
||||
assert!(auth_id.is_token());
|
||||
assert_eq!(
|
||||
auth_id.tokenname().expect("Token has tokenname").as_str(),
|
||||
TokennameRef::new("bar").as_str()
|
||||
);
|
||||
assert_eq!(auth_id.to_string(), "test@pam!bar".to_string());
|
||||
}
|
||||
|
||||
serde_plain::derive_deserialize_from_fromstr!(Userid, "valid user id");
|
||||
serde_plain::derive_serialize_from_display!(Userid);
|
||||
|
||||
serde_plain::derive_deserialize_from_fromstr!(Authid, "valid user id or token id");
|
||||
serde_plain::derive_serialize_from_display!(Authid);
|
@ -308,7 +308,7 @@ fn need_description(description: Option<String>) -> Result<String, Error> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_tfa_entry<A: OpenUserChallengeData>(
|
||||
config: &mut TfaConfig,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
description: Option<String>,
|
||||
totp: Option<String>,
|
||||
@ -410,9 +410,9 @@ fn add_yubico(
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_u2f<A: OpenUserChallengeData>(
|
||||
fn add_u2f<A: ?Sized + OpenUserChallengeData>(
|
||||
config: &mut TfaConfig,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
description: Option<String>,
|
||||
challenge: Option<String>,
|
||||
@ -436,9 +436,9 @@ fn add_u2f<A: OpenUserChallengeData>(
|
||||
}
|
||||
}
|
||||
|
||||
fn add_webauthn<A: OpenUserChallengeData>(
|
||||
fn add_webauthn<A: ?Sized + OpenUserChallengeData>(
|
||||
config: &mut TfaConfig,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
description: Option<String>,
|
||||
challenge: Option<String>,
|
||||
|
@ -43,22 +43,18 @@ trait IsExpired {
|
||||
fn is_expired(&self, at_epoch: i64) -> bool;
|
||||
}
|
||||
|
||||
pub trait OpenUserChallengeData: Clone {
|
||||
type Data: UserChallengeAccess;
|
||||
pub trait OpenUserChallengeData {
|
||||
fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error>;
|
||||
|
||||
fn open(&self, userid: &str) -> Result<Self::Data, Error>;
|
||||
|
||||
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>;
|
||||
|
||||
/// Should return `true` if something was removed, `false` if no data existed for the user.
|
||||
fn remove(&self, userid: &str) -> Result<bool, Error>;
|
||||
}
|
||||
|
||||
pub trait UserChallengeAccess: Sized {
|
||||
//fn open(userid: &str) -> Result<Self, Error>;
|
||||
//fn open_no_create(userid: &str) -> Result<Option<Self>, Error>;
|
||||
pub trait UserChallengeAccess {
|
||||
fn get_mut(&mut self) -> &mut TfaUserChallenges;
|
||||
fn save(self) -> Result<(), Error>;
|
||||
fn save(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
const CHALLENGE_TIMEOUT_SECS: i64 = 2 * 60;
|
||||
@ -115,9 +111,9 @@ fn check_webauthn<'a, 'config: 'a, 'origin: 'a>(
|
||||
|
||||
impl TfaConfig {
|
||||
// Get a u2f registration challenge.
|
||||
pub fn u2f_registration_challenge<A: OpenUserChallengeData>(
|
||||
pub fn u2f_registration_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
description: String,
|
||||
) -> Result<String, Error> {
|
||||
@ -130,9 +126,9 @@ impl TfaConfig {
|
||||
}
|
||||
|
||||
/// Finish a u2f registration challenge.
|
||||
pub fn u2f_registration_finish<A: OpenUserChallengeData>(
|
||||
pub fn u2f_registration_finish<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
challenge: &str,
|
||||
response: &str,
|
||||
@ -146,9 +142,9 @@ impl TfaConfig {
|
||||
}
|
||||
|
||||
/// Get a webauthn registration challenge.
|
||||
pub fn webauthn_registration_challenge<A: OpenUserChallengeData>(
|
||||
pub fn webauthn_registration_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
user: &str,
|
||||
description: String,
|
||||
origin: Option<&Url>,
|
||||
@ -162,9 +158,9 @@ impl TfaConfig {
|
||||
}
|
||||
|
||||
/// Finish a webauthn registration challenge.
|
||||
pub fn webauthn_registration_finish<A: OpenUserChallengeData>(
|
||||
pub fn webauthn_registration_finish<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
challenge: &str,
|
||||
response: &str,
|
||||
@ -215,9 +211,9 @@ impl TfaConfig {
|
||||
}
|
||||
|
||||
/// Get a two factor authentication challenge for a user, if the user has TFA set up.
|
||||
pub fn authentication_challenge<A: OpenUserChallengeData>(
|
||||
pub fn authentication_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
origin: Option<&Url>,
|
||||
) -> Result<Option<TfaChallenge>, Error> {
|
||||
@ -233,9 +229,9 @@ impl TfaConfig {
|
||||
}
|
||||
|
||||
/// Verify a TFA challenge.
|
||||
pub fn verify<A: OpenUserChallengeData>(
|
||||
pub fn verify<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
challenge: &TfaChallenge,
|
||||
response: TfaResponse,
|
||||
@ -266,9 +262,9 @@ impl TfaConfig {
|
||||
Ok(NeedsSaving::No)
|
||||
}
|
||||
|
||||
pub fn remove_user<A: OpenUserChallengeData>(
|
||||
pub fn remove_user<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
) -> Result<NeedsSaving, Error> {
|
||||
let mut save = access.remove(userid)?;
|
||||
@ -388,9 +384,9 @@ impl TfaUserData {
|
||||
/// challenges in the tfa config file if necessary. The user otherwise has no access to this
|
||||
/// information at this point, as the challenge is identified by its actual challenge data
|
||||
/// instead.
|
||||
fn u2f_registration_challenge<A: OpenUserChallengeData>(
|
||||
fn u2f_registration_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
u2f: &u2f::U2f,
|
||||
description: String,
|
||||
@ -409,9 +405,9 @@ impl TfaUserData {
|
||||
Ok(challenge)
|
||||
}
|
||||
|
||||
fn u2f_registration_finish<A: OpenUserChallengeData>(
|
||||
fn u2f_registration_finish<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
u2f: &u2f::U2f,
|
||||
challenge: &str,
|
||||
@ -434,9 +430,9 @@ impl TfaUserData {
|
||||
/// challenges in the tfa config file if necessary. The user otherwise has no access to this
|
||||
/// information at this point, as the challenge is identified by its actual challenge data
|
||||
/// instead.
|
||||
fn webauthn_registration_challenge<A: OpenUserChallengeData>(
|
||||
fn webauthn_registration_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
webauthn: Webauthn<WebauthnConfigInstance>,
|
||||
userid: &str,
|
||||
description: String,
|
||||
@ -473,9 +469,9 @@ impl TfaUserData {
|
||||
|
||||
/// Finish a webauthn registration. The challenge should correspond to an output of
|
||||
/// `webauthn_registration_challenge`. The response should come directly from the client.
|
||||
fn webauthn_registration_finish<A: OpenUserChallengeData>(
|
||||
fn webauthn_registration_finish<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
webauthn: Webauthn<WebauthnConfigInstance>,
|
||||
userid: &str,
|
||||
challenge: &str,
|
||||
@ -568,9 +564,9 @@ impl TfaUserData {
|
||||
}
|
||||
|
||||
/// Generate a generic TFA challenge. See the [`TfaChallenge`] description for details.
|
||||
fn challenge<A: OpenUserChallengeData>(
|
||||
fn challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
webauthn: Option<Result<Webauthn<WebauthnConfigInstance>, Error>>,
|
||||
u2f: Option<&u2f::U2f>,
|
||||
@ -583,7 +579,7 @@ impl TfaUserData {
|
||||
totp: self.totp.iter().any(|e| e.info.enable),
|
||||
recovery: RecoveryState::from(&self.recovery),
|
||||
webauthn: match webauthn {
|
||||
Some(webauthn) => self.webauthn_challenge(access.clone(), userid, webauthn?)?,
|
||||
Some(webauthn) => self.webauthn_challenge(access, userid, webauthn?)?,
|
||||
None => None,
|
||||
},
|
||||
u2f: match u2f {
|
||||
@ -600,9 +596,9 @@ impl TfaUserData {
|
||||
}
|
||||
|
||||
/// Generate an optional webauthn challenge.
|
||||
fn webauthn_challenge<A: OpenUserChallengeData>(
|
||||
fn webauthn_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
webauthn: Webauthn<WebauthnConfigInstance>,
|
||||
) -> Result<Option<webauthn_rs::proto::RequestChallengeResponse>, Error> {
|
||||
@ -632,9 +628,9 @@ impl TfaUserData {
|
||||
}
|
||||
|
||||
/// Generate an optional u2f challenge.
|
||||
fn u2f_challenge<A: OpenUserChallengeData>(
|
||||
fn u2f_challenge<A: ?Sized + OpenUserChallengeData>(
|
||||
&self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
u2f: &u2f::U2f,
|
||||
) -> Result<Option<U2fChallenge>, Error> {
|
||||
@ -666,9 +662,9 @@ impl TfaUserData {
|
||||
}
|
||||
|
||||
/// Verify a u2f response.
|
||||
fn verify_u2f<A: OpenUserChallengeData>(
|
||||
fn verify_u2f<A: ?Sized + OpenUserChallengeData>(
|
||||
&self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
u2f: u2f::U2f,
|
||||
challenge: &crate::u2f::AuthChallenge,
|
||||
@ -712,9 +708,9 @@ impl TfaUserData {
|
||||
}
|
||||
|
||||
/// Verify a webauthn response.
|
||||
fn verify_webauthn<A: OpenUserChallengeData>(
|
||||
fn verify_webauthn<A: ?Sized + OpenUserChallengeData>(
|
||||
&mut self,
|
||||
access: A,
|
||||
access: &A,
|
||||
userid: &str,
|
||||
webauthn: Webauthn<WebauthnConfigInstance>,
|
||||
mut response: Value,
|
||||
|
Loading…
Reference in New Issue
Block a user