rest server: cleanup auth-log handling

Handle auth logs the same way as access log.
- Configure with ApiConfig
- CommandoSocket command to reload auth-logs "api-auth-log-reopen"

Inside API calls, we now access the ApiConfig using the RestEnvironment.

The openid_login api now also logs failed logins and return http_err!(UNAUTHORIZED, ..)
on failed logins.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Dietmar Maurer
2021-09-21 07:58:50 +02:00
committed by Thomas Lamprecht
parent 1b1a553741
commit 36b7085ec2
7 changed files with 216 additions and 109 deletions

View File

@ -12,7 +12,7 @@ use proxmox::{http_err, list_subdirs_api_method};
use proxmox::{identity, sortable};
use pbs_api_types::{
Userid, Authid, PASSWORD_SCHEMA, ACL_PATH_SCHEMA,
Userid, Authid, PASSWORD_SCHEMA, ACL_PATH_SCHEMA,
PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT,
};
use pbs_tools::auth::private_auth_key;
@ -196,6 +196,12 @@ pub fn create_ticket(
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 worng RpcEnvironment type"))?;
match authenticate_user(&username, &password, path, privs, port, tfa_challenge) {
Ok(AuthResult::Success) => Ok(json!({ "username": username })),
Ok(AuthResult::CreateTicket) => {
@ -203,8 +209,7 @@ pub fn create_ticket(
let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
let token = assemble_csrf_prevention_token(csrf_secret(), &username);
crate::server::rest::auth_logger()?
.log(format!("successful auth for user '{}'", username));
env.log_auth(username.as_str());
Ok(json!({
"username": username,
@ -223,20 +228,7 @@ pub fn create_ticket(
}))
}
Err(err) => {
let client_ip = match rpcenv.get_client_ip().map(|addr| addr.ip()) {
Some(ip) => format!("{}", ip),
None => "unknown".into(),
};
let msg = format!(
"authentication failure; rhost={} user={} msg={}",
client_ip,
username,
err.to_string()
);
crate::server::rest::auth_logger()?.log(&msg);
log::error!("{}", msg);
env.log_failed_auth(Some(username.to_string()), &err.to_string());
Err(http_err!(UNAUTHORIZED, "permission check failed."))
}
}

View File

@ -1,14 +1,13 @@
//! OpenID redirect/login API
use std::convert::TryFrom;
use anyhow::{bail, Error};
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox::api::router::{Router, SubdirMap};
use proxmox::api::{api, Permission, RpcEnvironment};
use proxmox::{list_subdirs_api_method};
use proxmox::{identity, sortable};
use proxmox::{http_err, list_subdirs_api_method, identity, sortable};
use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig};
@ -80,76 +79,95 @@ pub fn openid_login(
state: String,
code: String,
redirect_url: String,
_rpcenv: &mut dyn RpcEnvironment,
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 worng RpcEnvironment type"))?;
let user_info = CachedUserInfo::new()?;
let (realm, private_auth_state) =
OpenIdAuthenticator::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M!(), &state)?;
let mut tested_username = None;
let (domains, _digest) = pbs_config::domains::config()?;
let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
let result = proxmox::try_block!({
let open_id = openid_authenticator(&config, &redirect_url)?;
let (realm, private_auth_state) =
OpenIdAuthenticator::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M!(), &state)?;
let info = open_id.verify_authorization_code(&code, &private_auth_state)?;
let (domains, _digest) = pbs_config::domains::config()?;
let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
// eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
let open_id = openid_authenticator(&config, &redirect_url)?;
let unique_name = match config.username_claim {
None | Some(OpenIdUserAttribute::Subject) => info.subject().as_str(),
Some(OpenIdUserAttribute::Username) => {
match info.preferred_username() {
Some(name) => name.as_str(),
None => bail!("missing claim 'preferred_name'"),
let info = open_id.verify_authorization_code(&code, &private_auth_state)?;
// eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
let unique_name = match config.username_claim {
None | Some(OpenIdUserAttribute::Subject) => info.subject().as_str(),
Some(OpenIdUserAttribute::Username) => {
match info.preferred_username() {
Some(name) => name.as_str(),
None => bail!("missing claim 'preferred_name'"),
}
}
Some(OpenIdUserAttribute::Email) => {
match info.email() {
Some(name) => name.as_str(),
None => bail!("missing claim 'email'"),
}
}
};
let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
tested_username = Some(unique_name.to_string());
if !user_info.is_active_user_id(&user_id) {
if config.autocreate.unwrap_or(false) {
use pbs_config::user;
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
let user = User {
userid: user_id.clone(),
comment: None,
enable: None,
expire: None,
firstname: info.given_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
lastname: info.family_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
email: info.email().map(|e| e.to_string()),
};
let (mut config, _digest) = user::config()?;
if config.sections.get(user.userid.as_str()).is_some() {
bail!("autocreate user failed - '{}' already exists.", user.userid);
}
config.set_data(user.userid.as_str(), "user", &user)?;
user::save_config(&config)?;
} else {
bail!("user account '{}' missing, disabled or expired.", user_id);
}
}
Some(OpenIdUserAttribute::Email) => {
match info.email() {
Some(name) => name.as_str(),
None => bail!("missing claim 'email'"),
}
}
};
let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
let api_ticket = ApiTicket::full(user_id.clone());
let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
if !user_info.is_active_user_id(&user_id) {
if config.autocreate.unwrap_or(false) {
use pbs_config::user;
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
let user = User {
userid: user_id.clone(),
comment: None,
enable: None,
expire: None,
firstname: info.given_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
lastname: info.family_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
email: info.email().map(|e| e.to_string()),
};
let (mut config, _digest) = user::config()?;
if config.sections.get(user.userid.as_str()).is_some() {
bail!("autocreate user failed - '{}' already exists.", user.userid);
}
config.set_data(user.userid.as_str(), "user", &user)?;
user::save_config(&config)?;
} else {
bail!("user account '{}' missing, disabled or expired.", user_id);
}
env.log_auth(user_id.as_str());
Ok(json!({
"username": user_id,
"ticket": ticket,
"CSRFPreventionToken": token,
}))
});
if let Err(ref err) = result {
let msg = err.to_string();
env.log_failed_auth(tested_username, &msg);
return Err(http_err!(UNAUTHORIZED, "{}", msg))
}
let api_ticket = ApiTicket::full(user_id.clone());
let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
crate::server::rest::auth_logger()?
.log(format!("successful auth for user '{}'", user_id));
Ok(json!({
"username": user_id,
"ticket": ticket,
"CSRFPreventionToken": token,
}))
result
}
#[api(