api2/access: implement term ticket

modeled after pves/pmgs vncticket (i substituted the vnc with term)
by putting the path and username as secret data in the ticket

when sending the ticket to /access/ticket it only verifies it,
checks the privs on the path and does not generate a new ticket

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
This commit is contained in:
Dominik Csapak 2020-07-21 11:10:37 +02:00 committed by Thomas Lamprecht
parent 2ab5acac5a
commit a4d1675513
2 changed files with 101 additions and 10 deletions

View File

@ -13,15 +13,22 @@ use crate::auth_helpers::*;
use crate::api2::types::*; use crate::api2::types::*;
use crate::config::cached_user_info::CachedUserInfo; use crate::config::cached_user_info::CachedUserInfo;
use crate::config::acl::PRIV_PERMISSIONS_MODIFY; use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY};
pub mod user; pub mod user;
pub mod domain; pub mod domain;
pub mod acl; pub mod acl;
pub mod role; pub mod role;
fn authenticate_user(username: &str, password: &str) -> Result<(), Error> { /// returns Ok(true) if a ticket has to be created
/// and Ok(false) if not
fn authenticate_user(
username: &str,
password: &str,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
) -> Result<bool, Error> {
let user_info = CachedUserInfo::new()?; let user_info = CachedUserInfo::new()?;
if !user_info.is_active_user(&username) { if !user_info.is_active_user(&username) {
@ -33,14 +40,43 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
if password.starts_with("PBS:") { if password.starts_with("PBS:") {
if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) { if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) {
if ticket_username == username { if ticket_username == username {
return Ok(()); return Ok(true);
} else { } else {
bail!("ticket login failed - wrong username"); bail!("ticket login failed - wrong username");
} }
} }
} 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");
} }
crate::auth::authenticate_user(username, password) let path = path.unwrap();
let privilege_name = privs.unwrap();
let port = port.unwrap();
if let Ok((_age, _data)) =
tools::ticket::verify_term_ticket(public_auth_key(), &username, &path, port, password)
{
for (name, privilege) in PRIVILEGES {
if *name == privilege_name {
let mut path_vec = Vec::new();
for part in path.split('/') {
if part != "" {
path_vec.push(part);
}
}
user_info.check_privs(username, &path_vec, *privilege, false)?;
return Ok(false);
}
}
bail!("No such privilege");
}
}
let _ = crate::auth::authenticate_user(username, password)?;
Ok(true)
} }
#[api( #[api(
@ -52,6 +88,21 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
password: { password: {
schema: PASSWORD_SCHEMA, 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,
},
}, },
}, },
returns: { returns: {
@ -78,10 +129,15 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
/// Create or verify authentication ticket. /// Create or verify authentication ticket.
/// ///
/// Returns: An authentication ticket with additional infos. /// Returns: An authentication ticket with additional infos.
fn create_ticket(username: String, password: String) -> Result<Value, Error> { fn create_ticket(
match authenticate_user(&username, &password) { username: String,
Ok(_) => { password: String,
path: Option<String>,
privs: Option<String>,
port: Option<u16>,
) -> Result<Value, Error> {
match authenticate_user(&username, &password, path, privs, port) {
Ok(true) => {
let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some(&username), None)?; let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some(&username), None)?;
let token = assemble_csrf_prevention_token(csrf_secret(), &username); let token = assemble_csrf_prevention_token(csrf_secret(), &username);
@ -94,6 +150,9 @@ fn create_ticket(username: String, password: String) -> Result<Value, Error> {
"CSRFPreventionToken": token, "CSRFPreventionToken": token,
})) }))
} }
Ok(false) => Ok(json!({
"username": username,
})),
Err(err) => { Err(err) => {
let client_ip = "unknown"; // $rpcenv->get_client_ip() || ''; let client_ip = "unknown"; // $rpcenv->get_client_ip() || '';
log::error!("authentication failure; rhost={} user={} msg={}", client_ip, username, err.to_string()); log::error!("authentication failure; rhost={} user={} msg={}", client_ip, username, err.to_string());

View File

@ -11,6 +11,38 @@ use crate::tools::epoch_now_u64;
pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours
const TERM_PREFIX: &str = "PBSTERM";
pub fn assemble_term_ticket(
keypair: &PKey<Private>,
username: &str,
path: &str,
port: u16,
) -> Result<String, Error> {
assemble_rsa_ticket(
keypair,
TERM_PREFIX,
None,
Some(&format!("{}{}{}", username, path, port)),
)
}
pub fn verify_term_ticket(
keypair: &PKey<Public>,
username: &str,
path: &str,
port: u16,
ticket: &str,
) -> Result<(i64, Option<String>), Error> {
verify_rsa_ticket(
keypair,
TERM_PREFIX,
ticket,
Some(&format!("{}{}{}", username, path, port)),
-300,
TICKET_LIFETIME,
)
}
pub fn assemble_rsa_ticket( pub fn assemble_rsa_ticket(
keypair: &PKey<Private>, keypair: &PKey<Private>,