diff --git a/server/core/src/actors/v1_read.rs b/server/core/src/actors/v1_read.rs index a6c8b2d02..b22bd782d 100644 --- a/server/core/src/actors/v1_read.rs +++ b/server/core/src/actors/v1_read.rs @@ -1,5 +1,6 @@ use std::convert::TryFrom; use std::fs; +use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -110,6 +111,7 @@ impl QueryServerReadV1 { sessionid: Option, req: AuthRequest, eventid: Uuid, + ip_addr: IpAddr, ) -> Result { // This is probably the first function that really implements logic // "on top" of the db server concept. In this case we check if @@ -132,10 +134,12 @@ impl QueryServerReadV1 { // the session are enforced. idm_auth.expire_auth_sessions(ct).await; + let source = Source::Https(ip_addr); + // Generally things like auth denied are in Ok() msgs // so true errors should always trigger a rollback. let res = idm_auth - .auth(&ae, ct) + .auth(&ae, ct, source) .await .and_then(|r| idm_auth.commit().map(|_| r)); @@ -155,6 +159,7 @@ impl QueryServerReadV1 { uat: Option, issue: AuthIssueSession, eventid: Uuid, + ip_addr: IpAddr, ) -> Result { let ct = duration_from_epoch_now(); let mut idm_auth = self.idms.auth().await; @@ -172,10 +177,12 @@ impl QueryServerReadV1 { // the session are enforced. idm_auth.expire_auth_sessions(ct).await; + let source = Source::Https(ip_addr); + // Generally things like auth denied are in Ok() msgs // so true errors should always trigger a rollback. let res = idm_auth - .reauth_init(ident, issue, ct) + .reauth_init(ident, issue, ct, source) .await .and_then(|r| idm_auth.commit().map(|_| r)); diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index d23e622da..eef181ab5 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -6,6 +6,7 @@ mod v1; mod v1_scim; use std::fs::canonicalize; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::str::FromStr; @@ -71,6 +72,7 @@ pub struct AppState { pub jws_validator: std::sync::Arc, /// The SHA384 hashes of javascript files we're going to serve to users pub js_files: Vec, + pub(crate) trust_x_forward_for: bool, } pub trait RequestExtensions { @@ -85,6 +87,8 @@ pub trait RequestExtensions { fn get_url_param_uuid(&self, param: &str) -> Result; fn new_eventid(&self) -> (Uuid, String); + + fn get_remote_addr(&self) -> Option; } impl RequestExtensions for tide::Request { @@ -178,6 +182,16 @@ impl RequestExtensions for tide::Request { let hv = eventid.as_hyphenated().to_string(); (eventid, hv) } + + fn get_remote_addr(&self) -> Option { + if self.state().trust_x_forward_for { + self.remote() + } else { + self.peer_addr() + } + .and_then(|add_str| add_str.parse().ok()) + .map(|s_ad: SocketAddr| s_ad.ip()) + } } pub fn to_tide_response( @@ -387,6 +401,7 @@ pub async fn create_https_server( jws_signer, jws_validator, js_files: js_files.to_owned(), + trust_x_forward_for, }); // Add the logging subsystem. diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 7f9b47736..77d9927ba 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -1068,6 +1068,14 @@ pub async fn reauth(mut req: tide::Request) -> tide::Result { let uat = req.get_current_uat(); let (eventid, hvalue) = req.new_eventid(); + let ip_addr = req.get_remote_addr().ok_or_else(|| { + error!("Unable to process remote addr, refusing to proceed"); + tide::Error::from_str( + tide::StatusCode::InternalServerError, + "unable to validate peer address", + ) + })?; + let obj: AuthIssueSession = req.body_json().await.map_err(|e| { debug!("Failed get body JSON? {:?}", e); e @@ -1077,7 +1085,7 @@ pub async fn reauth(mut req: tide::Request) -> tide::Result { .state() // This may change in the future ... .qe_r_ref - .handle_reauth(uat, obj, eventid) + .handle_reauth(uat, obj, eventid, ip_addr) .await; auth_session_state_management(req, inter, hvalue) @@ -1091,6 +1099,14 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { let maybe_sessionid: Option = req.get_current_auth_session_id(); + let ip_addr = req.get_remote_addr().ok_or_else(|| { + error!("Unable to process remote addr, refusing to proceed"); + tide::Error::from_str( + tide::StatusCode::InternalServerError, + "unable to validate peer address", + ) + })?; + let obj: AuthRequest = req.body_json().await.map_err(|e| { debug!("Failed get body JSON? {:?}", e); e @@ -1103,7 +1119,7 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { .state() // This may change in the future ... .qe_r_ref - .handle_auth(maybe_sessionid, obj, eventid) + .handle_auth(maybe_sessionid, obj, eventid, ip_addr) .await; auth_session_state_management(req, inter, hvalue) diff --git a/server/core/src/lib.rs b/server/core/src/lib.rs index 41db0dee2..8d2b6b419 100644 --- a/server/core/src/lib.rs +++ b/server/core/src/lib.rs @@ -39,7 +39,6 @@ use kanidm_proto::messages::{AccountChangeMessage, MessageStatus}; use kanidm_proto::v1::OperationError; use kanidmd_lib::be::{Backend, BackendConfig, BackendTransaction, FsType}; use kanidmd_lib::idm::ldap::LdapServer; -use kanidmd_lib::idm::server::{IdmServer, IdmServerDelayed}; use kanidmd_lib::prelude::*; use kanidmd_lib::schema::Schema; use kanidmd_lib::status::StatusActor; @@ -101,7 +100,7 @@ async fn setup_qs_idms( be: Backend, schema: Schema, config: &Configuration, -) -> Result<(QueryServer, IdmServer, IdmServerDelayed), OperationError> { +) -> Result<(QueryServer, IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> { // Create a query_server implementation let query_server = QueryServer::new(be, schema, config.domain.clone()); @@ -119,9 +118,10 @@ async fn setup_qs_idms( // We generate a SINGLE idms only! - let (idms, idms_delayed) = IdmServer::new(query_server.clone(), &config.origin).await?; + let (idms, idms_delayed, idms_audit) = + IdmServer::new(query_server.clone(), &config.origin).await?; - Ok((query_server, idms, idms_delayed)) + Ok((query_server, idms, idms_delayed, idms_audit)) } async fn setup_qs( @@ -297,7 +297,7 @@ pub async fn restore_server_core(config: &Configuration, dst_path: &str) { info!("Attempting to init query server ..."); - let (qs, _idms, _idms_delayed) = match setup_qs_idms(be, schema, config).await { + let (qs, _idms, _idms_delayed, _idms_audit) = match setup_qs_idms(be, schema, config).await { Ok(t) => t, Err(e) => { error!("Unable to setup query server or idm server -> {:?}", e); @@ -354,7 +354,7 @@ pub async fn reindex_server_core(config: &Configuration) { eprintln!("Attempting to init query server ..."); - let (qs, _idms, _idms_delayed) = match setup_qs_idms(be, schema, config).await { + let (qs, _idms, _idms_delayed, _idms_audit) = match setup_qs_idms(be, schema, config).await { Ok(t) => t, Err(e) => { error!("Unable to setup query server or idm server -> {:?}", e); @@ -515,7 +515,7 @@ pub async fn recover_account_core(config: &Configuration, name: &str) { } }; // setup the qs - *with* init of the migrations and schema. - let (_qs, idms, _idms_delayed) = match setup_qs_idms(be, schema, config).await { + let (_qs, idms, _idms_delayed, _idms_audit) = match setup_qs_idms(be, schema, config).await { Ok(t) => t, Err(e) => { error!("Unable to setup query server or idm server -> {:?}", e); @@ -651,13 +651,14 @@ pub async fn create_server_core( } }; // Start the IDM server. - let (_qs, idms, mut idms_delayed) = match setup_qs_idms(be, schema, &config).await { - Ok(t) => t, - Err(e) => { - error!("Unable to setup query server or idm server -> {:?}", e); - return Err(()); - } - }; + let (_qs, idms, mut idms_delayed, mut idms_audit) = + match setup_qs_idms(be, schema, &config).await { + Ok(t) => t, + Err(e) => { + error!("Unable to setup query server or idm server -> {:?}", e); + return Err(()); + } + }; // Extract any configuration from the IDMS that we may need. // For now we just do this per run, but we need to extract this from the db later. @@ -735,6 +736,33 @@ pub async fn create_server_core( info!("Stopped DelayedActionActor"); }); + let mut broadcast_rx = broadcast_tx.subscribe(); + + let auditd_handle = tokio::spawn(async move { + loop { + tokio::select! { + Ok(action) = broadcast_rx.recv() => { + match action { + CoreAction::Shutdown => break, + } + } + audit_event = idms_audit.audit_rx().recv() => { + match serde_json::to_string(&audit_event) { + Ok(audit_event) => { + warn!(%audit_event); + } + Err(e) => { + error!(err=?e, "Unable to process audit event to json."); + warn!(?audit_event, json=false); + } + } + + } + } + } + info!("Stopped AuditdActor"); + }); + // Setup timed events associated to the write thread let interval_handle = IntervalActor::start(server_write_ref, broadcast_tx.subscribe()); // Setup timed events associated to the read thread @@ -811,7 +839,7 @@ pub async fn create_server_core( Some(h) }; - let mut handles = vec![interval_handle, delayed_handle]; + let mut handles = vec![interval_handle, delayed_handle, auditd_handle]; if let Some(backup_handle) = maybe_backup_handle { handles.push(backup_handle) diff --git a/server/lib-macros/src/entry.rs b/server/lib-macros/src/entry.rs index ec2f56057..54910cd08 100644 --- a/server/lib-macros/src/entry.rs +++ b/server/lib-macros/src/entry.rs @@ -192,7 +192,9 @@ pub(crate) fn qs_pair_test(_args: &TokenStream, item: TokenStream, with_init: bo result.into() } -pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn idm_test(args: &TokenStream, item: TokenStream) -> TokenStream { + let audit = args.to_string() == "audit"; + let input: syn::ItemFn = match syn::parse(item.clone()) { Ok(it) => it, Err(e) => return token_stream_with_error(item, e), @@ -237,6 +239,16 @@ pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream { let test_fn = &input.sig.ident; let test_driver = Ident::new(&format!("idm_{}", test_fn), input.sig.span()); + let test_fn_args = if audit { + quote! { + &test_server, &mut idms_delayed, &mut idms_audit + } + } else { + quote! { + &test_server, &mut idms_delayed + } + }; + // Effectively we are just injecting a real test function around this which we will // call. @@ -246,9 +258,9 @@ pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream { #header fn #test_driver() { let body = async { - let (test_server, mut idms_delayed) = crate::testkit::setup_idm_test().await; + let (test_server, mut idms_delayed, mut idms_audit) = crate::testkit::setup_idm_test().await; - #test_fn(&test_server, &mut idms_delayed).await; + #test_fn(#test_fn_args).await; // Any needed teardown? // Make sure there are no errors. @@ -258,6 +270,7 @@ pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream { assert!(verifications.len() == 0); idms_delayed.check_is_empty_or_panic(); + idms_audit.check_is_empty_or_panic(); }; #[allow(clippy::expect_used, clippy::diverging_sub_expression)] { diff --git a/server/lib/src/idm/audit.rs b/server/lib/src/idm/audit.rs new file mode 100644 index 000000000..09c5df5c3 --- /dev/null +++ b/server/lib/src/idm/audit.rs @@ -0,0 +1,30 @@ +use crate::prelude::*; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum AuditSource { + Internal, + Https(IpAddr), +} + +impl From for AuditSource { + fn from(value: Source) -> Self { + match value { + Source::Internal => AuditSource::Internal, + Source::Https(ip) => AuditSource::Https(ip), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum AuditEvent { + AuthenticationDenied { + source: AuditSource, + uuid: Uuid, + spn: String, + #[serde(with = "time::serde::timestamp")] + time: OffsetDateTime, + }, +} diff --git a/server/lib/src/idm/authsession.rs b/server/lib/src/idm/authsession.rs index 27f013ef5..80a516064 100644 --- a/server/lib/src/idm/authsession.rs +++ b/server/lib/src/idm/authsession.rs @@ -28,6 +28,7 @@ use webauthn_rs::prelude::{ use crate::credential::totp::Totp; use crate::credential::{BackupCodes, Credential, CredentialType, Password}; use crate::idm::account::Account; +use crate::idm::audit::AuditEvent; use crate::idm::delayed::{ AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement, }; @@ -737,6 +738,9 @@ pub(crate) struct AuthSession { // What is the "intent" behind this auth session? Are we doing an initial auth? Or a re-auth // for a privilege grant? intent: AuthIntent, + + // Where did the event come from? + source: Source, } impl AuthSession { @@ -748,6 +752,7 @@ impl AuthSession { issue: AuthIssueSession, webauthn: &Webauthn, ct: Duration, + source: Source, ) -> (Option, AuthState) { // During this setup, determine the credential handler that we'll be using // for this session. This is currently based on presentation of an application @@ -803,6 +808,7 @@ impl AuthSession { state, issue, intent: AuthIntent::InitialAuth, + source, }; // Get the set of mechanisms that can proceed. This is tied // to the session so that it can mutate state and have progression @@ -827,6 +833,7 @@ impl AuthSession { issue: AuthIssueSession, webauthn: &Webauthn, ct: Duration, + source: Source, ) -> (Option, AuthState) { /// An inner enum to allow us to more easily define state within this fn enum State { @@ -893,6 +900,7 @@ impl AuthSession { session_id, session_expiry: session.expiry, }, + source, }; let as_state = AuthState::Continue(allow); @@ -995,6 +1003,7 @@ impl AuthSession { cred: &AuthCredential, time: Duration, async_tx: &Sender, + audit_tx: &Sender, webauthn: &Webauthn, pw_badlist_set: Option<&HashSet>, uat_jwt_signer: &JwsSigner, @@ -1041,6 +1050,17 @@ impl AuthSession { (None, Ok(AuthState::Continue(allowed.into_iter().collect()))) } CredState::Denied(reason) => { + if audit_tx + .send(AuditEvent::AuthenticationDenied { + source: self.source.clone().into(), + spn: self.account.spn.clone(), + uuid: self.account.uuid, + time: OffsetDateTime::UNIX_EPOCH + time, + }) + .is_err() + { + error!("Unable to submit audit event to queue"); + } security_info!(%reason, "Credentials denied"); ( Some(AuthSessionState::Denied(reason)), @@ -1204,6 +1224,7 @@ mod tests { use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP}; use crate::credential::{BackupCodes, Credential}; + use crate::idm::audit::AuditEvent; use crate::idm::authsession::{ AuthSession, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG, @@ -1246,6 +1267,7 @@ mod tests { AuthIssueSession::Token, &webauthn, duration_from_epoch_now(), + Source::Internal, ); if let AuthState::Choose(auth_mechs) = state { @@ -1279,6 +1301,7 @@ mod tests { AuthIssueSession::Token, $webauthn, duration_from_epoch_now(), + Source::Internal, ); let mut session = session.unwrap(); @@ -1316,6 +1339,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // now check let (mut session, pw_badlist_cache) = @@ -1327,6 +1351,7 @@ mod tests { &attempt, Duration::from_secs(0), &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1335,6 +1360,11 @@ mod tests { _ => panic!(), }; + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + // === Now begin a new session, and use a good pw. let (mut session, pw_badlist_cache) = @@ -1345,6 +1375,7 @@ mod tests { &attempt, Duration::from_secs(0), &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1360,6 +1391,8 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] @@ -1375,6 +1408,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // now check, even though the password is correct, Auth should be denied since it is in badlist let (mut session, pw_badlist_cache) = @@ -1385,6 +1419,7 @@ mod tests { &attempt, Duration::from_secs(0), &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1393,8 +1428,15 @@ mod tests { _ => panic!(), }; + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } macro_rules! start_password_mfa_session { @@ -1407,6 +1449,7 @@ mod tests { AuthIssueSession::Token, $webauthn, duration_from_epoch_now(), + Source::Internal, ); let mut session = session.expect("Session was unable to be created."); @@ -1487,6 +1530,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // now check @@ -1499,6 +1543,7 @@ mod tests { &AuthCredential::Anonymous, ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1506,6 +1551,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // == two step checks @@ -1519,6 +1569,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1526,6 +1577,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check send bad totp, should fail immediate { @@ -1536,6 +1592,7 @@ mod tests { &AuthCredential::Totp(totp_bad), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1543,6 +1600,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check send good totp, should continue @@ -1555,6 +1617,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1566,6 +1629,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1573,6 +1637,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check send good totp, should continue @@ -1585,6 +1654,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1596,6 +1666,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1612,6 +1683,8 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] @@ -1642,6 +1715,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // now check @@ -1657,6 +1731,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1668,6 +1743,7 @@ mod tests { &AuthCredential::Password(pw_badlist.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1675,10 +1751,17 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } macro_rules! start_webauthn_only_session { @@ -1692,6 +1775,7 @@ mod tests { AuthIssueSession::Token, $webauthn, duration_from_epoch_now(), + Source::Internal, ); let mut session = session.unwrap(); @@ -1782,6 +1866,7 @@ mod tests { fn test_idm_authsession_webauthn_only_mech() { sketching::test_init(); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); let ts = duration_from_epoch_now(); // create the ent let mut account = entry_to_account!(E_ADMIN_V1.clone()); @@ -1803,6 +1888,7 @@ mod tests { &AuthCredential::Anonymous, ts, &async_tx, + &audit_tx, &webauthn, None, &jws_signer, @@ -1810,6 +1896,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // Check good challenge @@ -1825,6 +1916,7 @@ mod tests { &AuthCredential::Passkey(resp), ts, &async_tx, + &audit_tx, &webauthn, None, &jws_signer, @@ -1859,6 +1951,7 @@ mod tests { &AuthCredential::Passkey(resp), ts, &async_tx, + &audit_tx, &webauthn, None, &jws_signer, @@ -1866,6 +1959,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // Use an incorrect softtoken. @@ -1902,6 +2000,7 @@ mod tests { &AuthCredential::Passkey(resp), ts, &async_tx, + &audit_tx, &webauthn, None, &jws_signer, @@ -1909,16 +2008,24 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] fn test_idm_authsession_webauthn_password_mech() { sketching::test_init(); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); let ts = duration_from_epoch_now(); // create the ent let mut account = entry_to_account!(E_ADMIN_V1); @@ -1946,6 +2053,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1953,6 +2061,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // Check totp first attempt fails. @@ -1964,6 +2077,7 @@ mod tests { &AuthCredential::Totp(0), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -1971,6 +2085,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check bad webauthn (fail) @@ -1993,6 +2112,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2000,6 +2120,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check good webauthn/bad pw (fail) @@ -2017,6 +2142,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2028,6 +2154,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2036,6 +2163,11 @@ mod tests { _ => panic!(), }; + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + // Check the async counter update was sent. match async_rx.blocking_recv() { Some(DelayedAction::WebauthnCounterIncrement(_)) => {} @@ -2058,6 +2190,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2069,6 +2202,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2090,12 +2224,15 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] fn test_idm_authsession_webauthn_password_totp_mech() { sketching::test_init(); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); let ts = duration_from_epoch_now(); // create the ent let mut account = entry_to_account!(E_ADMIN_V1); @@ -2134,6 +2271,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2141,6 +2279,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // Check bad totp (fail) @@ -2152,6 +2295,7 @@ mod tests { &AuthCredential::Totp(totp_bad), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2159,6 +2303,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check bad webauthn (fail) @@ -2179,6 +2328,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2186,6 +2336,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check good webauthn/bad pw (fail) @@ -2203,6 +2358,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2214,6 +2370,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2222,6 +2379,11 @@ mod tests { _ => panic!(), }; + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + // Check the async counter update was sent. match async_rx.blocking_recv() { Some(DelayedAction::WebauthnCounterIncrement(_)) => {} @@ -2238,6 +2400,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2249,6 +2412,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2256,6 +2420,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check good totp/good pw (pass) @@ -2267,6 +2436,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2278,6 +2448,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2307,6 +2478,7 @@ mod tests { &AuthCredential::SecurityKey(resp), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2318,6 +2490,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2339,6 +2512,8 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] @@ -2381,6 +2556,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // now check // == two step checks @@ -2394,6 +2570,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2401,6 +2578,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check send wrong backup code, should fail immediate { @@ -2411,6 +2593,7 @@ mod tests { &AuthCredential::BackupCode(backup_code_bad), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2418,6 +2601,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // check send good backup code, should continue // then bad pw, fail pw @@ -2429,6 +2617,7 @@ mod tests { &AuthCredential::BackupCode(backup_code_good.clone()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2440,6 +2629,7 @@ mod tests { &AuthCredential::Password(pw_bad.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2447,6 +2637,11 @@ mod tests { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), }; + + match audit_rx.try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } } // Can't process BackupCodeRemoval without the server instance match async_rx.blocking_recv() { @@ -2464,6 +2659,7 @@ mod tests { &AuthCredential::BackupCode(backup_code_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2475,6 +2671,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2506,6 +2703,7 @@ mod tests { &AuthCredential::Totp(totp_good), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2517,6 +2715,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2534,6 +2733,8 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } #[test] @@ -2574,6 +2775,7 @@ mod tests { account.primary = Some(cred); let (async_tx, mut async_rx) = unbounded(); + let (audit_tx, mut audit_rx) = unbounded(); // Test totp_a { @@ -2584,6 +2786,7 @@ mod tests { &AuthCredential::Totp(totp_good_a), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2595,6 +2798,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2618,6 +2822,7 @@ mod tests { &AuthCredential::Totp(totp_good_b), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2629,6 +2834,7 @@ mod tests { &AuthCredential::Password(pw_good.to_string()), ts, &async_tx, + &audit_tx, &webauthn, Some(&pw_badlist_cache), &jws_signer, @@ -2645,5 +2851,7 @@ mod tests { drop(async_tx); assert!(async_rx.blocking_recv().is_none()); + drop(audit_tx); + assert!(audit_rx.blocking_recv().is_none()); } } diff --git a/server/lib/src/idm/credupdatesession.rs b/server/lib/src/idm/credupdatesession.rs index 16f9365fb..8a9fe09e2 100644 --- a/server/lib/src/idm/credupdatesession.rs +++ b/server/lib/src/idm/credupdatesession.rs @@ -1762,7 +1762,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1773,7 +1773,7 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1782,7 +1782,7 @@ mod tests { let pw_step = AuthEvent::cred_step_password(sessionid, pw); // Expect success - let r2 = idms_auth.auth(&pw_step, ct).await; + let r2 = idms_auth.auth(&pw_step, ct, Source::Internal).await; debug!("r2 ==> {:?}", r2); idms_auth.commit().expect("Must not fail"); @@ -1812,7 +1812,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1823,7 +1823,7 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1834,7 +1834,7 @@ mod tests { .expect("Failed to perform totp step"); let totp_step = AuthEvent::cred_step_totp(sessionid, totp); - let r2 = idms_auth.auth(&totp_step, ct).await; + let r2 = idms_auth.auth(&totp_step, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1843,7 +1843,7 @@ mod tests { let pw_step = AuthEvent::cred_step_password(sessionid, pw); // Expect success - let r3 = idms_auth.auth(&pw_step, ct).await; + let r3 = idms_auth.auth(&pw_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -1872,7 +1872,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1883,14 +1883,14 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; assert!(matches!(state, AuthState::Continue(_))); let code_step = AuthEvent::cred_step_backup_code(sessionid, code); - let r2 = idms_auth.auth(&code_step, ct).await; + let r2 = idms_auth.auth(&code_step, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1899,7 +1899,7 @@ mod tests { let pw_step = AuthEvent::cred_step_password(sessionid, pw); // Expect success - let r3 = idms_auth.auth(&pw_step, ct).await; + let r3 = idms_auth.auth(&pw_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -1934,7 +1934,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1945,7 +1945,7 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -1967,7 +1967,7 @@ mod tests { let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); - let r3 = idms_auth.auth(&passkey_step, ct).await; + let r3 = idms_auth.auth(&passkey_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); diff --git a/server/lib/src/idm/mod.rs b/server/lib/src/idm/mod.rs index 201085cd6..1b1a07bff 100644 --- a/server/lib/src/idm/mod.rs +++ b/server/lib/src/idm/mod.rs @@ -5,6 +5,7 @@ pub mod account; pub mod applinks; +pub mod audit; pub mod authsession; pub mod credupdatesession; pub mod delayed; diff --git a/server/lib/src/idm/reauth.rs b/server/lib/src/idm/reauth.rs index c423842b4..80524bbc7 100644 --- a/server/lib/src/idm/reauth.rs +++ b/server/lib/src/idm/reauth.rs @@ -23,6 +23,7 @@ impl<'a> IdmServerAuthTransaction<'a> { ident: Identity, issue: AuthIssueSession, ct: Duration, + source: Source, ) -> Result { // re-auth only works on users, so lets get the user account. // hint - it's in the ident! @@ -138,6 +139,7 @@ impl<'a> IdmServerAuthTransaction<'a> { issue, self.webauthn, ct, + source, ); // Push the re-auth session to the session maps. @@ -168,6 +170,7 @@ impl<'a> IdmServerAuthTransaction<'a> { #[cfg(test)] mod tests { use crate::credential::totp::Totp; + use crate::idm::audit::AuditEvent; use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus}; use crate::idm::delayed::DelayedAction; use crate::idm::event::{AuthEvent, AuthResult}; @@ -331,7 +334,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -342,7 +345,7 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -364,7 +367,7 @@ mod tests { let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); - let r3 = idms_auth.auth(&passkey_step, ct).await; + let r3 = idms_auth.auth(&passkey_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -404,7 +407,7 @@ mod tests { let auth_init = AuthEvent::named_init("testperson"); - let r1 = idms_auth.auth(&auth_init, ct).await; + let r1 = idms_auth.auth(&auth_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -415,7 +418,7 @@ mod tests { let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); - let r2 = idms_auth.auth(&auth_begin, ct).await; + let r2 = idms_auth.auth(&auth_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -426,7 +429,7 @@ mod tests { .expect("Failed to perform totp step"); let totp_step = AuthEvent::cred_step_totp(sessionid, totp); - let r2 = idms_auth.auth(&totp_step, ct).await; + let r2 = idms_auth.auth(&totp_step, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -435,7 +438,7 @@ mod tests { let pw_step = AuthEvent::cred_step_password(sessionid, pw); // Expect success - let r3 = idms_auth.auth(&pw_step, ct).await; + let r3 = idms_auth.auth(&pw_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -477,7 +480,7 @@ mod tests { let origin = idms_auth.get_origin().clone(); let auth_allowed = idms_auth - .reauth_init(ident.clone(), AuthIssueSession::Token, ct) + .reauth_init(ident.clone(), AuthIssueSession::Token, ct, Source::Internal) .await .expect("Failed to start reauth."); @@ -501,7 +504,7 @@ mod tests { let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); - let r3 = idms_auth.auth(&passkey_step, ct).await; + let r3 = idms_auth.auth(&passkey_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -536,7 +539,7 @@ mod tests { let mut idms_auth = idms.auth().await; let auth_allowed = idms_auth - .reauth_init(ident.clone(), AuthIssueSession::Token, ct) + .reauth_init(ident.clone(), AuthIssueSession::Token, ct, Source::Internal) .await .expect("Failed to start reauth."); @@ -561,7 +564,7 @@ mod tests { .expect("Failed to perform totp step"); let totp_step = AuthEvent::cred_step_totp(sessionid, totp); - let r2 = idms_auth.auth(&totp_step, ct).await; + let r2 = idms_auth.auth(&totp_step, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -570,7 +573,7 @@ mod tests { let pw_step = AuthEvent::cred_step_password(sessionid, pw); // Expect success - let r3 = idms_auth.auth(&pw_step, ct).await; + let r3 = idms_auth.auth(&pw_step, ct, Source::Internal).await; debug!("r3 ==> {:?}", r3); idms_auth.commit().expect("Must not fail"); @@ -628,8 +631,12 @@ mod tests { assert!(matches!(ident.access_scope(), AccessScope::ReadWrite)); } - #[idm_test] - async fn test_idm_reauth_softlocked_pw(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { + #[idm_test(audit)] + async fn test_idm_reauth_softlocked_pw( + idms: &IdmServer, + idms_delayed: &mut IdmServerDelayed, + idms_audit: &mut IdmServerAudit, + ) { // This test is to enforce that an account in a soft lock state can't proceed // we a re-auth. let ct = duration_from_epoch_now(); @@ -668,6 +675,12 @@ mod tests { .await .is_none()); + // There should be a queued audit event + match idms_audit.audit_rx().try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + // Start the re-auth - MUST FAIL! assert!( reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed) diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index 3711f63c4..68be27875 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -29,6 +29,7 @@ use super::event::ReadBackupCodeEvent; use super::ldap::{LdapBoundToken, LdapSession}; use crate::credential::{softlock::CredSoftLock, Credential}; use crate::idm::account::Account; +use crate::idm::audit::AuditEvent; use crate::idm::authsession::AuthSession; use crate::idm::credupdatesession::CredentialUpdateSessionMutex; use crate::idm::delayed::{ @@ -82,6 +83,7 @@ pub struct IdmServer { /// The configured crypto policy for the IDM server. Later this could be transactional and loaded from the db similar to access. But today it's just to allow dynamic pbkdf2rounds crypto_policy: CryptoPolicy, async_tx: Sender, + audit_tx: Sender, /// [Webauthn] verifier/config webauthn: Webauthn, pw_badlist_cache: Arc>>, @@ -100,6 +102,7 @@ pub struct IdmServerAuthTransaction<'a> { pub(crate) sid: Sid, // For flagging eventual actions. pub(crate) async_tx: Sender, + pub(crate) audit_tx: Sender, pub(crate) webauthn: &'a Webauthn, pub(crate) pw_badlist_cache: CowCellReadTxn>, pub(crate) domain_keys: CowCellReadTxn, @@ -120,7 +123,6 @@ pub struct IdmServerProxyReadTransaction<'a> { pub qs_read: QueryServerReadTransaction<'a>, pub(crate) domain_keys: CowCellReadTxn, pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction, - // pub(crate) async_tx: Sender, } pub struct IdmServerProxyWriteTransaction<'a> { @@ -141,12 +143,15 @@ pub struct IdmServerDelayed { pub(crate) async_rx: Receiver, } +pub struct IdmServerAudit { + pub(crate) audit_rx: Receiver, +} + impl IdmServer { - // TODO: Make number of authsessions configurable!!! pub async fn new( qs: QueryServer, origin: &str, - ) -> Result<(IdmServer, IdmServerDelayed), OperationError> { + ) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> { // This is calculated back from: // 500 auths / thread -> 0.002 sec per op // we can then spend up to ~0.001s hashing @@ -156,6 +161,7 @@ impl IdmServer { // improves. let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(1)); let (async_tx, async_rx) = unbounded(); + let (audit_tx, audit_rx) = unbounded(); // Get the domain name, as the relying party id. let ( @@ -249,12 +255,14 @@ impl IdmServer { qs, crypto_policy, async_tx, + audit_tx, webauthn, pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)), domain_keys, oauth2rs: Arc::new(oauth2rs), }, IdmServerDelayed { async_rx }, + IdmServerAudit { audit_rx }, )) } @@ -277,6 +285,7 @@ impl IdmServer { qs_read, sid, async_tx: self.async_tx.clone(), + audit_tx: self.audit_tx.clone(), webauthn: &self.webauthn, pw_badlist_cache: self.pw_badlist_cache.read(), domain_keys: self.domain_keys.read(), @@ -339,30 +348,44 @@ impl IdmServer { } } -impl IdmServerDelayed { - // I think we can just make this async in the future? +impl IdmServerAudit { #[cfg(test)] pub(crate) fn check_is_empty_or_panic(&mut self) { - use core::task::{Context, Poll}; - use futures::task as futures_task; + use tokio::sync::mpsc::error::TryRecvError; - let waker = futures_task::noop_waker(); - let mut cx = Context::from_waker(&waker); - match self.async_rx.poll_recv(&mut cx) { - Poll::Pending | Poll::Ready(None) => {} - Poll::Ready(Some(m)) => { + match self.audit_rx.try_recv() { + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + panic!("Task queue disconnected"); + } + Ok(m) => { trace!(?m); - panic!("Task queue not empty") + panic!("Task queue not empty"); } } } - /* - #[cfg(test)] - pub(crate) fn blocking_recv(&mut self) -> Option { - self.async_rx.blocking_recv() + pub fn audit_rx(&mut self) -> &mut Receiver { + &mut self.audit_rx + } +} + +impl IdmServerDelayed { + #[cfg(test)] + pub(crate) fn check_is_empty_or_panic(&mut self) { + use tokio::sync::mpsc::error::TryRecvError; + + match self.async_rx.try_recv() { + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + panic!("Task queue disconnected"); + } + Ok(m) => { + trace!(?m); + panic!("Task queue not empty"); + } + } } - */ #[cfg(test)] pub(crate) fn try_recv(&mut self) -> Result { @@ -909,6 +932,7 @@ impl<'a> IdmServerAuthTransaction<'a> { &mut self, ae: &AuthEvent, ct: Duration, + source: Source, ) -> Result { // Match on the auth event, to see what we need to do. match &ae.step { @@ -983,7 +1007,7 @@ impl<'a> IdmServerAuthTransaction<'a> { }); let (auth_session, state) = - AuthSession::new(account, init.issue, self.webauthn, ct); + AuthSession::new(account, init.issue, self.webauthn, ct, source); match auth_session { Some(auth_session) => { @@ -1106,6 +1130,7 @@ impl<'a> IdmServerAuthTransaction<'a> { &creds.cred, ct, &self.async_tx, + &self.audit_tx, self.webauthn, pw_badlist_cache, &self.domain_keys.uat_jwt_signer, @@ -2050,6 +2075,7 @@ mod tests { use crate::credential::{Credential, Password}; use crate::idm::account::DestroySessionTokenEvent; + use crate::idm::audit::AuditEvent; use crate::idm::delayed::{AuthSessionRecord, DelayedAction}; use crate::idm::event::{AuthEvent, AuthResult}; use crate::idm::event::{ @@ -2075,7 +2101,11 @@ mod tests { let anon_init = AuthEvent::anonymous_init(); // Expect success let r1 = idms_auth - .auth(&anon_init, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_init, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; /* Some weird lifetime things happen here ... */ @@ -2113,7 +2143,11 @@ mod tests { let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous); let r2 = idms_auth - .auth(&anon_begin, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_begin, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2151,7 +2185,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2195,7 +2233,11 @@ mod tests { // Expect failure let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2239,7 +2281,7 @@ mod tests { let mut idms_auth = idms.auth().await; let admin_init = AuthEvent::named_init(name); - let r1 = idms_auth.auth(&admin_init, ct).await; + let r1 = idms_auth.auth(&admin_init, ct, Source::Internal).await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -2248,7 +2290,7 @@ mod tests { // Now push that we want the Password Mech. let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); - let r2 = idms_auth.auth(&admin_begin, ct).await; + let r2 = idms_auth.auth(&admin_begin, ct, Source::Internal).await; let ar = r2.unwrap(); let AuthResult { sessionid, state } = ar; @@ -2274,7 +2316,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2342,7 +2388,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2377,8 +2427,12 @@ mod tests { idms_auth.commit().expect("Must not fail"); } - #[idm_test] - async fn test_idm_simple_password_invalid(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { + #[idm_test(audit)] + async fn test_idm_simple_password_invalid( + idms: &IdmServer, + _idms_delayed: &IdmServerDelayed, + idms_audit: &mut IdmServerAudit, + ) { init_admin_w_password(idms, TEST_PASSWORD) .await .expect("Failed to setup admin account"); @@ -2389,7 +2443,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -2416,6 +2474,12 @@ mod tests { } }; + // There should be a queued audit event + match idms_audit.audit_rx().try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + idms_auth.commit().expect("Must not fail"); } @@ -2838,7 +2902,9 @@ mod tests { let mut idms_auth = idms.auth().await; let admin_init = AuthEvent::named_init("admin"); - let r1 = idms_auth.auth(&admin_init, time_low).await; + let r1 = idms_auth + .auth(&admin_init, time_low, Source::Internal) + .await; let ar = r1.unwrap(); let AuthResult { @@ -2858,7 +2924,9 @@ mod tests { // And here! let mut idms_auth = idms.auth().await; let admin_init = AuthEvent::named_init("admin"); - let r1 = idms_auth.auth(&admin_init, time_high).await; + let r1 = idms_auth + .auth(&admin_init, time_high, Source::Internal) + .await; let ar = r1.unwrap(); let AuthResult { @@ -2987,8 +3055,12 @@ mod tests { } } - #[idm_test] - async fn test_idm_account_softlocking(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { + #[idm_test(audit)] + async fn test_idm_account_softlocking( + idms: &IdmServer, + idms_delayed: &mut IdmServerDelayed, + idms_audit: &mut IdmServerAudit, + ) { init_admin_w_password(idms, TEST_PASSWORD) .await .expect("Failed to setup admin account"); @@ -3000,7 +3072,11 @@ mod tests { let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -3025,6 +3101,13 @@ mod tests { panic!(); } }; + + // There should be a queued audit event + match idms_audit.audit_rx().try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + idms_auth.commit().expect("Must not fail"); // Auth init, softlock present, count == 1, same time (so before unlock_at) @@ -3034,7 +3117,11 @@ mod tests { let admin_init = AuthEvent::named_init("admin"); let r1 = idms_auth - .auth(&admin_init, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &admin_init, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; let ar = r1.unwrap(); let AuthResult { sessionid, state } = ar; @@ -3044,7 +3131,11 @@ mod tests { let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); let r2 = idms_auth - .auth(&admin_begin, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &admin_begin, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; let ar = r2.unwrap(); let AuthResult { @@ -3077,7 +3168,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME + 2)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME + 2), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -3119,10 +3214,11 @@ mod tests { // Tested in the softlock state machine. } - #[idm_test] + #[idm_test(audit)] async fn test_idm_account_softlocking_interleaved( idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed, + idms_audit: &mut IdmServerAudit, ) { init_admin_w_password(idms, TEST_PASSWORD) .await @@ -3140,7 +3236,11 @@ mod tests { let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC); let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); @@ -3165,6 +3265,12 @@ mod tests { panic!(); } }; + + match idms_audit.audit_rx().try_recv() { + Ok(AuditEvent::AuthenticationDenied { .. }) => {} + _ => assert!(false), + } + idms_auth.commit().expect("Must not fail"); // Now check that sid_early is denied due to softlock. @@ -3173,7 +3279,11 @@ mod tests { // Expect success let r2 = idms_auth - .auth(&anon_step, Duration::from_secs(TEST_CURRENT_TIME)) + .auth( + &anon_step, + Duration::from_secs(TEST_CURRENT_TIME), + Source::Internal, + ) .await; debug!("r2 ==> {:?}", r2); match r2 { diff --git a/server/lib/src/lib.rs b/server/lib/src/lib.rs index 88cf7c980..53abfb9d4 100644 --- a/server/lib/src/lib.rs +++ b/server/lib/src/lib.rs @@ -78,13 +78,15 @@ pub mod prelude { f_and, f_andnot, f_eq, f_id, f_inc, f_lt, f_or, f_pres, f_self, f_spn_name, f_sub, Filter, FilterInvalid, FilterValid, FC, }; - pub use crate::idm::server::{IdmServer, IdmServerDelayed}; + pub use crate::idm::server::{IdmServer, IdmServerAudit, IdmServerDelayed}; pub use crate::modify::{ m_assert, m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList, ModifyValid, }; pub use crate::server::access::AccessControlsTransaction; pub use crate::server::batch_modify::BatchModifyEvent; - pub use crate::server::identity::{AccessScope, IdentType, IdentUser, Identity, IdentityId}; + pub use crate::server::identity::{ + AccessScope, IdentType, IdentUser, Identity, IdentityId, Source, + }; pub use crate::server::{ QueryServer, QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, diff --git a/server/lib/src/server/identity.rs b/server/lib/src/server/identity.rs index 73028ecf9..daf595798 100644 --- a/server/lib/src/server/identity.rs +++ b/server/lib/src/server/identity.rs @@ -6,6 +6,7 @@ use crate::be::Limits; use std::collections::BTreeSet; use std::hash::Hash; +use std::net::IpAddr; use std::sync::Arc; use uuid::uuid; @@ -16,6 +17,13 @@ use serde::{Deserialize, Serialize}; use crate::prelude::*; use crate::value::Session; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Source { + Internal, + Https(IpAddr), + // Ldaps, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccessScope { ReadOnly, diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 852877017..07fcdf066 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -1037,8 +1037,6 @@ impl QueryServer { let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap)); - // log_event!(log, "Starting query worker ..."); - #[allow(clippy::expect_used)] QueryServer { phase, diff --git a/server/lib/src/testkit.rs b/server/lib/src/testkit.rs index eb9457a16..b748cbd7b 100644 --- a/server/lib/src/testkit.rs +++ b/server/lib/src/testkit.rs @@ -57,7 +57,7 @@ pub async fn setup_pair_test() -> (QueryServer, QueryServer) { } #[allow(clippy::expect_used)] -pub async fn setup_idm_test() -> (IdmServer, IdmServerDelayed) { +pub async fn setup_idm_test() -> (IdmServer, IdmServerDelayed, IdmServerAudit) { let qs = setup_test().await; qs.initialise_helper(duration_from_epoch_now())