Absolutely minimal implementation (#1711)

* Absolutely minimal implementation

* Add support for ip address to audit event
This commit is contained in:
Firstyear 2023-06-08 20:17:46 +10:00 committed by GitHub
parent 152bf95e71
commit 0ba4aec86b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 545 additions and 96 deletions

View file

@ -1,5 +1,6 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs; use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@ -110,6 +111,7 @@ impl QueryServerReadV1 {
sessionid: Option<Uuid>, sessionid: Option<Uuid>,
req: AuthRequest, req: AuthRequest,
eventid: Uuid, eventid: Uuid,
ip_addr: IpAddr,
) -> Result<AuthResult, OperationError> { ) -> Result<AuthResult, OperationError> {
// This is probably the first function that really implements logic // This is probably the first function that really implements logic
// "on top" of the db server concept. In this case we check if // "on top" of the db server concept. In this case we check if
@ -132,10 +134,12 @@ impl QueryServerReadV1 {
// the session are enforced. // the session are enforced.
idm_auth.expire_auth_sessions(ct).await; idm_auth.expire_auth_sessions(ct).await;
let source = Source::Https(ip_addr);
// Generally things like auth denied are in Ok() msgs // Generally things like auth denied are in Ok() msgs
// so true errors should always trigger a rollback. // so true errors should always trigger a rollback.
let res = idm_auth let res = idm_auth
.auth(&ae, ct) .auth(&ae, ct, source)
.await .await
.and_then(|r| idm_auth.commit().map(|_| r)); .and_then(|r| idm_auth.commit().map(|_| r));
@ -155,6 +159,7 @@ impl QueryServerReadV1 {
uat: Option<String>, uat: Option<String>,
issue: AuthIssueSession, issue: AuthIssueSession,
eventid: Uuid, eventid: Uuid,
ip_addr: IpAddr,
) -> Result<AuthResult, OperationError> { ) -> Result<AuthResult, OperationError> {
let ct = duration_from_epoch_now(); let ct = duration_from_epoch_now();
let mut idm_auth = self.idms.auth().await; let mut idm_auth = self.idms.auth().await;
@ -172,10 +177,12 @@ impl QueryServerReadV1 {
// the session are enforced. // the session are enforced.
idm_auth.expire_auth_sessions(ct).await; idm_auth.expire_auth_sessions(ct).await;
let source = Source::Https(ip_addr);
// Generally things like auth denied are in Ok() msgs // Generally things like auth denied are in Ok() msgs
// so true errors should always trigger a rollback. // so true errors should always trigger a rollback.
let res = idm_auth let res = idm_auth
.reauth_init(ident, issue, ct) .reauth_init(ident, issue, ct, source)
.await .await
.and_then(|r| idm_auth.commit().map(|_| r)); .and_then(|r| idm_auth.commit().map(|_| r));

View file

@ -6,6 +6,7 @@ mod v1;
mod v1_scim; mod v1_scim;
use std::fs::canonicalize; use std::fs::canonicalize;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@ -71,6 +72,7 @@ pub struct AppState {
pub jws_validator: std::sync::Arc<JwsValidator>, pub jws_validator: std::sync::Arc<JwsValidator>,
/// The SHA384 hashes of javascript files we're going to serve to users /// The SHA384 hashes of javascript files we're going to serve to users
pub js_files: Vec<JavaScriptFile>, pub js_files: Vec<JavaScriptFile>,
pub(crate) trust_x_forward_for: bool,
} }
pub trait RequestExtensions { pub trait RequestExtensions {
@ -85,6 +87,8 @@ pub trait RequestExtensions {
fn get_url_param_uuid(&self, param: &str) -> Result<Uuid, tide::Error>; fn get_url_param_uuid(&self, param: &str) -> Result<Uuid, tide::Error>;
fn new_eventid(&self) -> (Uuid, String); fn new_eventid(&self) -> (Uuid, String);
fn get_remote_addr(&self) -> Option<IpAddr>;
} }
impl RequestExtensions for tide::Request<AppState> { impl RequestExtensions for tide::Request<AppState> {
@ -178,6 +182,16 @@ impl RequestExtensions for tide::Request<AppState> {
let hv = eventid.as_hyphenated().to_string(); let hv = eventid.as_hyphenated().to_string();
(eventid, hv) (eventid, hv)
} }
fn get_remote_addr(&self) -> Option<IpAddr> {
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<T: Serialize>( pub fn to_tide_response<T: Serialize>(
@ -387,6 +401,7 @@ pub async fn create_https_server(
jws_signer, jws_signer,
jws_validator, jws_validator,
js_files: js_files.to_owned(), js_files: js_files.to_owned(),
trust_x_forward_for,
}); });
// Add the logging subsystem. // Add the logging subsystem.

View file

@ -1068,6 +1068,14 @@ pub async fn reauth(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat(); let uat = req.get_current_uat();
let (eventid, hvalue) = req.new_eventid(); 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| { let obj: AuthIssueSession = req.body_json().await.map_err(|e| {
debug!("Failed get body JSON? {:?}", e); debug!("Failed get body JSON? {:?}", e);
e e
@ -1077,7 +1085,7 @@ pub async fn reauth(mut req: tide::Request<AppState>) -> tide::Result {
.state() .state()
// This may change in the future ... // This may change in the future ...
.qe_r_ref .qe_r_ref
.handle_reauth(uat, obj, eventid) .handle_reauth(uat, obj, eventid, ip_addr)
.await; .await;
auth_session_state_management(req, inter, hvalue) auth_session_state_management(req, inter, hvalue)
@ -1091,6 +1099,14 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
let maybe_sessionid: Option<Uuid> = req.get_current_auth_session_id(); let maybe_sessionid: Option<Uuid> = 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| { let obj: AuthRequest = req.body_json().await.map_err(|e| {
debug!("Failed get body JSON? {:?}", e); debug!("Failed get body JSON? {:?}", e);
e e
@ -1103,7 +1119,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
.state() .state()
// This may change in the future ... // This may change in the future ...
.qe_r_ref .qe_r_ref
.handle_auth(maybe_sessionid, obj, eventid) .handle_auth(maybe_sessionid, obj, eventid, ip_addr)
.await; .await;
auth_session_state_management(req, inter, hvalue) auth_session_state_management(req, inter, hvalue)

View file

@ -39,7 +39,6 @@ use kanidm_proto::messages::{AccountChangeMessage, MessageStatus};
use kanidm_proto::v1::OperationError; use kanidm_proto::v1::OperationError;
use kanidmd_lib::be::{Backend, BackendConfig, BackendTransaction, FsType}; use kanidmd_lib::be::{Backend, BackendConfig, BackendTransaction, FsType};
use kanidmd_lib::idm::ldap::LdapServer; use kanidmd_lib::idm::ldap::LdapServer;
use kanidmd_lib::idm::server::{IdmServer, IdmServerDelayed};
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
use kanidmd_lib::schema::Schema; use kanidmd_lib::schema::Schema;
use kanidmd_lib::status::StatusActor; use kanidmd_lib::status::StatusActor;
@ -101,7 +100,7 @@ async fn setup_qs_idms(
be: Backend, be: Backend,
schema: Schema, schema: Schema,
config: &Configuration, config: &Configuration,
) -> Result<(QueryServer, IdmServer, IdmServerDelayed), OperationError> { ) -> Result<(QueryServer, IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
// Create a query_server implementation // Create a query_server implementation
let query_server = QueryServer::new(be, schema, config.domain.clone()); 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! // 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( 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 ..."); 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, Ok(t) => t,
Err(e) => { Err(e) => {
error!("Unable to setup query server or idm server -> {:?}", 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 ..."); 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, Ok(t) => t,
Err(e) => { Err(e) => {
error!("Unable to setup query server or idm server -> {:?}", 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. // 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, Ok(t) => t,
Err(e) => { Err(e) => {
error!("Unable to setup query server or idm server -> {:?}", e); error!("Unable to setup query server or idm server -> {:?}", e);
@ -651,7 +651,8 @@ pub async fn create_server_core(
} }
}; };
// Start the IDM server. // Start the IDM server.
let (_qs, idms, mut idms_delayed) = match setup_qs_idms(be, schema, &config).await { let (_qs, idms, mut idms_delayed, mut idms_audit) =
match setup_qs_idms(be, schema, &config).await {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
error!("Unable to setup query server or idm server -> {:?}", e); error!("Unable to setup query server or idm server -> {:?}", e);
@ -735,6 +736,33 @@ pub async fn create_server_core(
info!("Stopped DelayedActionActor"); 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 // Setup timed events associated to the write thread
let interval_handle = IntervalActor::start(server_write_ref, broadcast_tx.subscribe()); let interval_handle = IntervalActor::start(server_write_ref, broadcast_tx.subscribe());
// Setup timed events associated to the read thread // Setup timed events associated to the read thread
@ -811,7 +839,7 @@ pub async fn create_server_core(
Some(h) 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 { if let Some(backup_handle) = maybe_backup_handle {
handles.push(backup_handle) handles.push(backup_handle)

View file

@ -192,7 +192,9 @@ pub(crate) fn qs_pair_test(_args: &TokenStream, item: TokenStream, with_init: bo
result.into() 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()) { let input: syn::ItemFn = match syn::parse(item.clone()) {
Ok(it) => it, Ok(it) => it,
Err(e) => return token_stream_with_error(item, e), 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_fn = &input.sig.ident;
let test_driver = Ident::new(&format!("idm_{}", test_fn), input.sig.span()); 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 // Effectively we are just injecting a real test function around this which we will
// call. // call.
@ -246,9 +258,9 @@ pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream {
#header #header
fn #test_driver() { fn #test_driver() {
let body = async { 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? // Any needed teardown?
// Make sure there are no errors. // Make sure there are no errors.
@ -258,6 +270,7 @@ pub(crate) fn idm_test(_args: &TokenStream, item: TokenStream) -> TokenStream {
assert!(verifications.len() == 0); assert!(verifications.len() == 0);
idms_delayed.check_is_empty_or_panic(); idms_delayed.check_is_empty_or_panic();
idms_audit.check_is_empty_or_panic();
}; };
#[allow(clippy::expect_used, clippy::diverging_sub_expression)] #[allow(clippy::expect_used, clippy::diverging_sub_expression)]
{ {

View file

@ -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<Source> 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,
},
}

View file

@ -28,6 +28,7 @@ use webauthn_rs::prelude::{
use crate::credential::totp::Totp; use crate::credential::totp::Totp;
use crate::credential::{BackupCodes, Credential, CredentialType, Password}; use crate::credential::{BackupCodes, Credential, CredentialType, Password};
use crate::idm::account::Account; use crate::idm::account::Account;
use crate::idm::audit::AuditEvent;
use crate::idm::delayed::{ use crate::idm::delayed::{
AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement, 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 // What is the "intent" behind this auth session? Are we doing an initial auth? Or a re-auth
// for a privilege grant? // for a privilege grant?
intent: AuthIntent, intent: AuthIntent,
// Where did the event come from?
source: Source,
} }
impl AuthSession { impl AuthSession {
@ -748,6 +752,7 @@ impl AuthSession {
issue: AuthIssueSession, issue: AuthIssueSession,
webauthn: &Webauthn, webauthn: &Webauthn,
ct: Duration, ct: Duration,
source: Source,
) -> (Option<Self>, AuthState) { ) -> (Option<Self>, AuthState) {
// During this setup, determine the credential handler that we'll be using // During this setup, determine the credential handler that we'll be using
// for this session. This is currently based on presentation of an application // for this session. This is currently based on presentation of an application
@ -803,6 +808,7 @@ impl AuthSession {
state, state,
issue, issue,
intent: AuthIntent::InitialAuth, intent: AuthIntent::InitialAuth,
source,
}; };
// Get the set of mechanisms that can proceed. This is tied // Get the set of mechanisms that can proceed. This is tied
// to the session so that it can mutate state and have progression // to the session so that it can mutate state and have progression
@ -827,6 +833,7 @@ impl AuthSession {
issue: AuthIssueSession, issue: AuthIssueSession,
webauthn: &Webauthn, webauthn: &Webauthn,
ct: Duration, ct: Duration,
source: Source,
) -> (Option<Self>, AuthState) { ) -> (Option<Self>, AuthState) {
/// An inner enum to allow us to more easily define state within this fn /// An inner enum to allow us to more easily define state within this fn
enum State { enum State {
@ -893,6 +900,7 @@ impl AuthSession {
session_id, session_id,
session_expiry: session.expiry, session_expiry: session.expiry,
}, },
source,
}; };
let as_state = AuthState::Continue(allow); let as_state = AuthState::Continue(allow);
@ -995,6 +1003,7 @@ impl AuthSession {
cred: &AuthCredential, cred: &AuthCredential,
time: Duration, time: Duration,
async_tx: &Sender<DelayedAction>, async_tx: &Sender<DelayedAction>,
audit_tx: &Sender<AuditEvent>,
webauthn: &Webauthn, webauthn: &Webauthn,
pw_badlist_set: Option<&HashSet<String>>, pw_badlist_set: Option<&HashSet<String>>,
uat_jwt_signer: &JwsSigner, uat_jwt_signer: &JwsSigner,
@ -1041,6 +1050,17 @@ impl AuthSession {
(None, Ok(AuthState::Continue(allowed.into_iter().collect()))) (None, Ok(AuthState::Continue(allowed.into_iter().collect())))
} }
CredState::Denied(reason) => { 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"); security_info!(%reason, "Credentials denied");
( (
Some(AuthSessionState::Denied(reason)), Some(AuthSessionState::Denied(reason)),
@ -1204,6 +1224,7 @@ mod tests {
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP}; use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
use crate::credential::{BackupCodes, Credential}; use crate::credential::{BackupCodes, Credential};
use crate::idm::audit::AuditEvent;
use crate::idm::authsession::{ use crate::idm::authsession::{
AuthSession, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG, AuthSession, BAD_AUTH_TYPE_MSG, BAD_BACKUPCODE_MSG, BAD_PASSWORD_MSG, BAD_TOTP_MSG,
BAD_WEBAUTHN_MSG, PW_BADLIST_MSG, BAD_WEBAUTHN_MSG, PW_BADLIST_MSG,
@ -1246,6 +1267,7 @@ mod tests {
AuthIssueSession::Token, AuthIssueSession::Token,
&webauthn, &webauthn,
duration_from_epoch_now(), duration_from_epoch_now(),
Source::Internal,
); );
if let AuthState::Choose(auth_mechs) = state { if let AuthState::Choose(auth_mechs) = state {
@ -1279,6 +1301,7 @@ mod tests {
AuthIssueSession::Token, AuthIssueSession::Token,
$webauthn, $webauthn,
duration_from_epoch_now(), duration_from_epoch_now(),
Source::Internal,
); );
let mut session = session.unwrap(); let mut session = session.unwrap();
@ -1316,6 +1339,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
// now check // now check
let (mut session, pw_badlist_cache) = let (mut session, pw_badlist_cache) =
@ -1327,6 +1351,7 @@ mod tests {
&attempt, &attempt,
Duration::from_secs(0), Duration::from_secs(0),
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1335,6 +1360,11 @@ mod tests {
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
// === Now begin a new session, and use a good pw. // === Now begin a new session, and use a good pw.
let (mut session, pw_badlist_cache) = let (mut session, pw_badlist_cache) =
@ -1345,6 +1375,7 @@ mod tests {
&attempt, &attempt,
Duration::from_secs(0), Duration::from_secs(0),
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1360,6 +1391,8 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
@ -1375,6 +1408,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); 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 // now check, even though the password is correct, Auth should be denied since it is in badlist
let (mut session, pw_badlist_cache) = let (mut session, pw_badlist_cache) =
@ -1385,6 +1419,7 @@ mod tests {
&attempt, &attempt,
Duration::from_secs(0), Duration::from_secs(0),
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1393,8 +1428,15 @@ mod tests {
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
macro_rules! start_password_mfa_session { macro_rules! start_password_mfa_session {
@ -1407,6 +1449,7 @@ mod tests {
AuthIssueSession::Token, AuthIssueSession::Token,
$webauthn, $webauthn,
duration_from_epoch_now(), duration_from_epoch_now(),
Source::Internal,
); );
let mut session = session.expect("Session was unable to be created."); let mut session = session.expect("Session was unable to be created.");
@ -1487,6 +1530,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
// now check // now check
@ -1499,6 +1543,7 @@ mod tests {
&AuthCredential::Anonymous, &AuthCredential::Anonymous,
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1506,6 +1551,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// == two step checks // == two step checks
@ -1519,6 +1569,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1526,6 +1577,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check send bad totp, should fail immediate // check send bad totp, should fail immediate
{ {
@ -1536,6 +1592,7 @@ mod tests {
&AuthCredential::Totp(totp_bad), &AuthCredential::Totp(totp_bad),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1543,6 +1600,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check send good totp, should continue // check send good totp, should continue
@ -1555,6 +1617,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1566,6 +1629,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1573,6 +1637,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check send good totp, should continue // check send good totp, should continue
@ -1585,6 +1654,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1596,6 +1666,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1612,6 +1683,8 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
@ -1642,6 +1715,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
// now check // now check
@ -1657,6 +1731,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1668,6 +1743,7 @@ mod tests {
&AuthCredential::Password(pw_badlist.to_string()), &AuthCredential::Password(pw_badlist.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1675,10 +1751,17 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
macro_rules! start_webauthn_only_session { macro_rules! start_webauthn_only_session {
@ -1692,6 +1775,7 @@ mod tests {
AuthIssueSession::Token, AuthIssueSession::Token,
$webauthn, $webauthn,
duration_from_epoch_now(), duration_from_epoch_now(),
Source::Internal,
); );
let mut session = session.unwrap(); let mut session = session.unwrap();
@ -1782,6 +1866,7 @@ mod tests {
fn test_idm_authsession_webauthn_only_mech() { fn test_idm_authsession_webauthn_only_mech() {
sketching::test_init(); sketching::test_init();
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now(); let ts = duration_from_epoch_now();
// create the ent // create the ent
let mut account = entry_to_account!(E_ADMIN_V1.clone()); let mut account = entry_to_account!(E_ADMIN_V1.clone());
@ -1803,6 +1888,7 @@ mod tests {
&AuthCredential::Anonymous, &AuthCredential::Anonymous,
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
None, None,
&jws_signer, &jws_signer,
@ -1810,6 +1896,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// Check good challenge // Check good challenge
@ -1825,6 +1916,7 @@ mod tests {
&AuthCredential::Passkey(resp), &AuthCredential::Passkey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
None, None,
&jws_signer, &jws_signer,
@ -1859,6 +1951,7 @@ mod tests {
&AuthCredential::Passkey(resp), &AuthCredential::Passkey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
None, None,
&jws_signer, &jws_signer,
@ -1866,6 +1959,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// Use an incorrect softtoken. // Use an incorrect softtoken.
@ -1902,6 +2000,7 @@ mod tests {
&AuthCredential::Passkey(resp), &AuthCredential::Passkey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
None, None,
&jws_signer, &jws_signer,
@ -1909,16 +2008,24 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
fn test_idm_authsession_webauthn_password_mech() { fn test_idm_authsession_webauthn_password_mech() {
sketching::test_init(); sketching::test_init();
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now(); let ts = duration_from_epoch_now();
// create the ent // create the ent
let mut account = entry_to_account!(E_ADMIN_V1); let mut account = entry_to_account!(E_ADMIN_V1);
@ -1946,6 +2053,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1953,6 +2061,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// Check totp first attempt fails. // Check totp first attempt fails.
@ -1964,6 +2077,7 @@ mod tests {
&AuthCredential::Totp(0), &AuthCredential::Totp(0),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -1971,6 +2085,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check bad webauthn (fail) // check bad webauthn (fail)
@ -1993,6 +2112,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2000,6 +2120,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check good webauthn/bad pw (fail) // check good webauthn/bad pw (fail)
@ -2017,6 +2142,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2028,6 +2154,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2036,6 +2163,11 @@ mod tests {
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
// Check the async counter update was sent. // Check the async counter update was sent.
match async_rx.blocking_recv() { match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {} Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
@ -2058,6 +2190,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2069,6 +2202,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2090,12 +2224,15 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
fn test_idm_authsession_webauthn_password_totp_mech() { fn test_idm_authsession_webauthn_password_totp_mech() {
sketching::test_init(); sketching::test_init();
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
let ts = duration_from_epoch_now(); let ts = duration_from_epoch_now();
// create the ent // create the ent
let mut account = entry_to_account!(E_ADMIN_V1); let mut account = entry_to_account!(E_ADMIN_V1);
@ -2134,6 +2271,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2141,6 +2279,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// Check bad totp (fail) // Check bad totp (fail)
@ -2152,6 +2295,7 @@ mod tests {
&AuthCredential::Totp(totp_bad), &AuthCredential::Totp(totp_bad),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2159,6 +2303,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check bad webauthn (fail) // check bad webauthn (fail)
@ -2179,6 +2328,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2186,6 +2336,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check good webauthn/bad pw (fail) // check good webauthn/bad pw (fail)
@ -2203,6 +2358,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2214,6 +2370,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2222,6 +2379,11 @@ mod tests {
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
// Check the async counter update was sent. // Check the async counter update was sent.
match async_rx.blocking_recv() { match async_rx.blocking_recv() {
Some(DelayedAction::WebauthnCounterIncrement(_)) => {} Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
@ -2238,6 +2400,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2249,6 +2412,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2256,6 +2420,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check good totp/good pw (pass) // check good totp/good pw (pass)
@ -2267,6 +2436,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2278,6 +2448,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2307,6 +2478,7 @@ mod tests {
&AuthCredential::SecurityKey(resp), &AuthCredential::SecurityKey(resp),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2318,6 +2490,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2339,6 +2512,8 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
@ -2381,6 +2556,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
// now check // now check
// == two step checks // == two step checks
@ -2394,6 +2570,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2401,6 +2578,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check send wrong backup code, should fail immediate // check send wrong backup code, should fail immediate
{ {
@ -2411,6 +2593,7 @@ mod tests {
&AuthCredential::BackupCode(backup_code_bad), &AuthCredential::BackupCode(backup_code_bad),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2418,6 +2601,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// check send good backup code, should continue // check send good backup code, should continue
// then bad pw, fail pw // then bad pw, fail pw
@ -2429,6 +2617,7 @@ mod tests {
&AuthCredential::BackupCode(backup_code_good.clone()), &AuthCredential::BackupCode(backup_code_good.clone()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2440,6 +2629,7 @@ mod tests {
&AuthCredential::Password(pw_bad.to_string()), &AuthCredential::Password(pw_bad.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2447,6 +2637,11 @@ mod tests {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
_ => panic!(), _ => panic!(),
}; };
match audit_rx.try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
} }
// Can't process BackupCodeRemoval without the server instance // Can't process BackupCodeRemoval without the server instance
match async_rx.blocking_recv() { match async_rx.blocking_recv() {
@ -2464,6 +2659,7 @@ mod tests {
&AuthCredential::BackupCode(backup_code_good), &AuthCredential::BackupCode(backup_code_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2475,6 +2671,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2506,6 +2703,7 @@ mod tests {
&AuthCredential::Totp(totp_good), &AuthCredential::Totp(totp_good),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2517,6 +2715,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2534,6 +2733,8 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
#[test] #[test]
@ -2574,6 +2775,7 @@ mod tests {
account.primary = Some(cred); account.primary = Some(cred);
let (async_tx, mut async_rx) = unbounded(); let (async_tx, mut async_rx) = unbounded();
let (audit_tx, mut audit_rx) = unbounded();
// Test totp_a // Test totp_a
{ {
@ -2584,6 +2786,7 @@ mod tests {
&AuthCredential::Totp(totp_good_a), &AuthCredential::Totp(totp_good_a),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2595,6 +2798,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2618,6 +2822,7 @@ mod tests {
&AuthCredential::Totp(totp_good_b), &AuthCredential::Totp(totp_good_b),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2629,6 +2834,7 @@ mod tests {
&AuthCredential::Password(pw_good.to_string()), &AuthCredential::Password(pw_good.to_string()),
ts, ts,
&async_tx, &async_tx,
&audit_tx,
&webauthn, &webauthn,
Some(&pw_badlist_cache), Some(&pw_badlist_cache),
&jws_signer, &jws_signer,
@ -2645,5 +2851,7 @@ mod tests {
drop(async_tx); drop(async_tx);
assert!(async_rx.blocking_recv().is_none()); assert!(async_rx.blocking_recv().is_none());
drop(audit_tx);
assert!(audit_rx.blocking_recv().is_none());
} }
} }

View file

@ -1762,7 +1762,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1773,7 +1773,7 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1782,7 +1782,7 @@ mod tests {
let pw_step = AuthEvent::cred_step_password(sessionid, pw); let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success // 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); debug!("r2 ==> {:?}", r2);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -1812,7 +1812,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1823,7 +1823,7 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1834,7 +1834,7 @@ mod tests {
.expect("Failed to perform totp step"); .expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1843,7 +1843,7 @@ mod tests {
let pw_step = AuthEvent::cred_step_password(sessionid, pw); let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success // 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -1872,7 +1872,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1883,14 +1883,14 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_))); assert!(matches!(state, AuthState::Continue(_)));
let code_step = AuthEvent::cred_step_backup_code(sessionid, code); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1899,7 +1899,7 @@ mod tests {
let pw_step = AuthEvent::cred_step_password(sessionid, pw); let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success // 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -1934,7 +1934,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1945,7 +1945,7 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -1967,7 +1967,7 @@ mod tests {
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");

View file

@ -5,6 +5,7 @@
pub mod account; pub mod account;
pub mod applinks; pub mod applinks;
pub mod audit;
pub mod authsession; pub mod authsession;
pub mod credupdatesession; pub mod credupdatesession;
pub mod delayed; pub mod delayed;

View file

@ -23,6 +23,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
ident: Identity, ident: Identity,
issue: AuthIssueSession, issue: AuthIssueSession,
ct: Duration, ct: Duration,
source: Source,
) -> Result<AuthResult, OperationError> { ) -> Result<AuthResult, OperationError> {
// re-auth only works on users, so lets get the user account. // re-auth only works on users, so lets get the user account.
// hint - it's in the ident! // hint - it's in the ident!
@ -138,6 +139,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
issue, issue,
self.webauthn, self.webauthn,
ct, ct,
source,
); );
// Push the re-auth session to the session maps. // Push the re-auth session to the session maps.
@ -168,6 +170,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::credential::totp::Totp; use crate::credential::totp::Totp;
use crate::idm::audit::AuditEvent;
use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus}; use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus};
use crate::idm::delayed::DelayedAction; use crate::idm::delayed::DelayedAction;
use crate::idm::event::{AuthEvent, AuthResult}; use crate::idm::event::{AuthEvent, AuthResult};
@ -331,7 +334,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -342,7 +345,7 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::Passkey); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -364,7 +367,7 @@ mod tests {
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -404,7 +407,7 @@ mod tests {
let auth_init = AuthEvent::named_init("testperson"); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -415,7 +418,7 @@ mod tests {
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -426,7 +429,7 @@ mod tests {
.expect("Failed to perform totp step"); .expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -435,7 +438,7 @@ mod tests {
let pw_step = AuthEvent::cred_step_password(sessionid, pw); let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success // 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -477,7 +480,7 @@ mod tests {
let origin = idms_auth.get_origin().clone(); let origin = idms_auth.get_origin().clone();
let auth_allowed = idms_auth let auth_allowed = idms_auth
.reauth_init(ident.clone(), AuthIssueSession::Token, ct) .reauth_init(ident.clone(), AuthIssueSession::Token, ct, Source::Internal)
.await .await
.expect("Failed to start reauth."); .expect("Failed to start reauth.");
@ -501,7 +504,7 @@ mod tests {
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp); 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -536,7 +539,7 @@ mod tests {
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let auth_allowed = idms_auth let auth_allowed = idms_auth
.reauth_init(ident.clone(), AuthIssueSession::Token, ct) .reauth_init(ident.clone(), AuthIssueSession::Token, ct, Source::Internal)
.await .await
.expect("Failed to start reauth."); .expect("Failed to start reauth.");
@ -561,7 +564,7 @@ mod tests {
.expect("Failed to perform totp step"); .expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -570,7 +573,7 @@ mod tests {
let pw_step = AuthEvent::cred_step_password(sessionid, pw); let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success // 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); debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
@ -628,8 +631,12 @@ mod tests {
assert!(matches!(ident.access_scope(), AccessScope::ReadWrite)); assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
} }
#[idm_test] #[idm_test(audit)]
async fn test_idm_reauth_softlocked_pw(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { 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 // This test is to enforce that an account in a soft lock state can't proceed
// we a re-auth. // we a re-auth.
let ct = duration_from_epoch_now(); let ct = duration_from_epoch_now();
@ -668,6 +675,12 @@ mod tests {
.await .await
.is_none()); .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! // Start the re-auth - MUST FAIL!
assert!( assert!(
reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed) reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)

View file

@ -29,6 +29,7 @@ use super::event::ReadBackupCodeEvent;
use super::ldap::{LdapBoundToken, LdapSession}; use super::ldap::{LdapBoundToken, LdapSession};
use crate::credential::{softlock::CredSoftLock, Credential}; use crate::credential::{softlock::CredSoftLock, Credential};
use crate::idm::account::Account; use crate::idm::account::Account;
use crate::idm::audit::AuditEvent;
use crate::idm::authsession::AuthSession; use crate::idm::authsession::AuthSession;
use crate::idm::credupdatesession::CredentialUpdateSessionMutex; use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
use crate::idm::delayed::{ 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 /// 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, crypto_policy: CryptoPolicy,
async_tx: Sender<DelayedAction>, async_tx: Sender<DelayedAction>,
audit_tx: Sender<AuditEvent>,
/// [Webauthn] verifier/config /// [Webauthn] verifier/config
webauthn: Webauthn, webauthn: Webauthn,
pw_badlist_cache: Arc<CowCell<HashSet<String>>>, pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
@ -100,6 +102,7 @@ pub struct IdmServerAuthTransaction<'a> {
pub(crate) sid: Sid, pub(crate) sid: Sid,
// For flagging eventual actions. // For flagging eventual actions.
pub(crate) async_tx: Sender<DelayedAction>, pub(crate) async_tx: Sender<DelayedAction>,
pub(crate) audit_tx: Sender<AuditEvent>,
pub(crate) webauthn: &'a Webauthn, pub(crate) webauthn: &'a Webauthn,
pub(crate) pw_badlist_cache: CowCellReadTxn<HashSet<String>>, pub(crate) pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>, pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
@ -120,7 +123,6 @@ pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>, pub qs_read: QueryServerReadTransaction<'a>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>, pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction, pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
// pub(crate) async_tx: Sender<DelayedAction>,
} }
pub struct IdmServerProxyWriteTransaction<'a> { pub struct IdmServerProxyWriteTransaction<'a> {
@ -141,12 +143,15 @@ pub struct IdmServerDelayed {
pub(crate) async_rx: Receiver<DelayedAction>, pub(crate) async_rx: Receiver<DelayedAction>,
} }
pub struct IdmServerAudit {
pub(crate) audit_rx: Receiver<AuditEvent>,
}
impl IdmServer { impl IdmServer {
// TODO: Make number of authsessions configurable!!!
pub async fn new( pub async fn new(
qs: QueryServer, qs: QueryServer,
origin: &str, origin: &str,
) -> Result<(IdmServer, IdmServerDelayed), OperationError> { ) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
// This is calculated back from: // This is calculated back from:
// 500 auths / thread -> 0.002 sec per op // 500 auths / thread -> 0.002 sec per op
// we can then spend up to ~0.001s hashing // we can then spend up to ~0.001s hashing
@ -156,6 +161,7 @@ impl IdmServer {
// improves. // improves.
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(1)); let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(1));
let (async_tx, async_rx) = unbounded(); let (async_tx, async_rx) = unbounded();
let (audit_tx, audit_rx) = unbounded();
// Get the domain name, as the relying party id. // Get the domain name, as the relying party id.
let ( let (
@ -249,12 +255,14 @@ impl IdmServer {
qs, qs,
crypto_policy, crypto_policy,
async_tx, async_tx,
audit_tx,
webauthn, webauthn,
pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)), pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)),
domain_keys, domain_keys,
oauth2rs: Arc::new(oauth2rs), oauth2rs: Arc::new(oauth2rs),
}, },
IdmServerDelayed { async_rx }, IdmServerDelayed { async_rx },
IdmServerAudit { audit_rx },
)) ))
} }
@ -277,6 +285,7 @@ impl IdmServer {
qs_read, qs_read,
sid, sid,
async_tx: self.async_tx.clone(), async_tx: self.async_tx.clone(),
audit_tx: self.audit_tx.clone(),
webauthn: &self.webauthn, webauthn: &self.webauthn,
pw_badlist_cache: self.pw_badlist_cache.read(), pw_badlist_cache: self.pw_badlist_cache.read(),
domain_keys: self.domain_keys.read(), domain_keys: self.domain_keys.read(),
@ -339,30 +348,44 @@ impl IdmServer {
} }
} }
impl IdmServerDelayed { impl IdmServerAudit {
// I think we can just make this async in the future?
#[cfg(test)] #[cfg(test)]
pub(crate) fn check_is_empty_or_panic(&mut self) { pub(crate) fn check_is_empty_or_panic(&mut self) {
use core::task::{Context, Poll}; use tokio::sync::mpsc::error::TryRecvError;
use futures::task as futures_task;
let waker = futures_task::noop_waker(); match self.audit_rx.try_recv() {
let mut cx = Context::from_waker(&waker); Err(TryRecvError::Empty) => {}
match self.async_rx.poll_recv(&mut cx) { Err(TryRecvError::Disconnected) => {
Poll::Pending | Poll::Ready(None) => {} panic!("Task queue disconnected");
Poll::Ready(Some(m)) => { }
Ok(m) => {
trace!(?m); trace!(?m);
panic!("Task queue not empty") panic!("Task queue not empty");
} }
} }
} }
/* pub fn audit_rx(&mut self) -> &mut Receiver<AuditEvent> {
#[cfg(test)] &mut self.audit_rx
pub(crate) fn blocking_recv(&mut self) -> Option<DelayedAction> { }
self.async_rx.blocking_recv() }
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)] #[cfg(test)]
pub(crate) fn try_recv(&mut self) -> Result<DelayedAction, OperationError> { pub(crate) fn try_recv(&mut self) -> Result<DelayedAction, OperationError> {
@ -909,6 +932,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
&mut self, &mut self,
ae: &AuthEvent, ae: &AuthEvent,
ct: Duration, ct: Duration,
source: Source,
) -> Result<AuthResult, OperationError> { ) -> Result<AuthResult, OperationError> {
// Match on the auth event, to see what we need to do. // Match on the auth event, to see what we need to do.
match &ae.step { match &ae.step {
@ -983,7 +1007,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
}); });
let (auth_session, state) = let (auth_session, state) =
AuthSession::new(account, init.issue, self.webauthn, ct); AuthSession::new(account, init.issue, self.webauthn, ct, source);
match auth_session { match auth_session {
Some(auth_session) => { Some(auth_session) => {
@ -1106,6 +1130,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
&creds.cred, &creds.cred,
ct, ct,
&self.async_tx, &self.async_tx,
&self.audit_tx,
self.webauthn, self.webauthn,
pw_badlist_cache, pw_badlist_cache,
&self.domain_keys.uat_jwt_signer, &self.domain_keys.uat_jwt_signer,
@ -2050,6 +2075,7 @@ mod tests {
use crate::credential::{Credential, Password}; use crate::credential::{Credential, Password};
use crate::idm::account::DestroySessionTokenEvent; use crate::idm::account::DestroySessionTokenEvent;
use crate::idm::audit::AuditEvent;
use crate::idm::delayed::{AuthSessionRecord, DelayedAction}; use crate::idm::delayed::{AuthSessionRecord, DelayedAction};
use crate::idm::event::{AuthEvent, AuthResult}; use crate::idm::event::{AuthEvent, AuthResult};
use crate::idm::event::{ use crate::idm::event::{
@ -2075,7 +2101,11 @@ mod tests {
let anon_init = AuthEvent::anonymous_init(); let anon_init = AuthEvent::anonymous_init();
// Expect success // Expect success
let r1 = idms_auth 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; .await;
/* Some weird lifetime things happen here ... */ /* Some weird lifetime things happen here ... */
@ -2113,7 +2143,11 @@ mod tests {
let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous); let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous);
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -2151,7 +2185,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -2195,7 +2233,11 @@ mod tests {
// Expect failure // Expect failure
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -2239,7 +2281,7 @@ mod tests {
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let admin_init = AuthEvent::named_init(name); 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 ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -2248,7 +2290,7 @@ mod tests {
// Now push that we want the Password Mech. // Now push that we want the Password Mech.
let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); 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 ar = r2.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -2274,7 +2316,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -2342,7 +2388,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -2377,8 +2427,12 @@ mod tests {
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
} }
#[idm_test] #[idm_test(audit)]
async fn test_idm_simple_password_invalid(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) { async fn test_idm_simple_password_invalid(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
) {
init_admin_w_password(idms, TEST_PASSWORD) init_admin_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
@ -2389,7 +2443,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); 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"); idms_auth.commit().expect("Must not fail");
} }
@ -2838,7 +2902,9 @@ mod tests {
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let admin_init = AuthEvent::named_init("admin"); 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 ar = r1.unwrap();
let AuthResult { let AuthResult {
@ -2858,7 +2924,9 @@ mod tests {
// And here! // And here!
let mut idms_auth = idms.auth().await; let mut idms_auth = idms.auth().await;
let admin_init = AuthEvent::named_init("admin"); 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 ar = r1.unwrap();
let AuthResult { let AuthResult {
@ -2987,8 +3055,12 @@ mod tests {
} }
} }
#[idm_test] #[idm_test(audit)]
async fn test_idm_account_softlocking(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) { async fn test_idm_account_softlocking(
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
) {
init_admin_w_password(idms, TEST_PASSWORD) init_admin_w_password(idms, TEST_PASSWORD)
.await .await
.expect("Failed to setup admin account"); .expect("Failed to setup admin account");
@ -3000,7 +3072,11 @@ mod tests {
let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -3025,6 +3101,13 @@ mod tests {
panic!(); 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"); idms_auth.commit().expect("Must not fail");
// Auth init, softlock present, count == 1, same time (so before unlock_at) // 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 admin_init = AuthEvent::named_init("admin");
let r1 = idms_auth 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; .await;
let ar = r1.unwrap(); let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar; let AuthResult { sessionid, state } = ar;
@ -3044,7 +3131,11 @@ mod tests {
let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password); let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);
let r2 = idms_auth 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; .await;
let ar = r2.unwrap(); let ar = r2.unwrap();
let AuthResult { let AuthResult {
@ -3077,7 +3168,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -3119,10 +3214,11 @@ mod tests {
// Tested in the softlock state machine. // Tested in the softlock state machine.
} }
#[idm_test] #[idm_test(audit)]
async fn test_idm_account_softlocking_interleaved( async fn test_idm_account_softlocking_interleaved(
idms: &IdmServer, idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed, _idms_delayed: &mut IdmServerDelayed,
idms_audit: &mut IdmServerAudit,
) { ) {
init_admin_w_password(idms, TEST_PASSWORD) init_admin_w_password(idms, TEST_PASSWORD)
.await .await
@ -3140,7 +3236,11 @@ mod tests {
let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC); let anon_step = AuthEvent::cred_step_password(sid_later, TEST_PASSWORD_INC);
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
@ -3165,6 +3265,12 @@ mod tests {
panic!(); panic!();
} }
}; };
match idms_audit.audit_rx().try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => assert!(false),
}
idms_auth.commit().expect("Must not fail"); idms_auth.commit().expect("Must not fail");
// Now check that sid_early is denied due to softlock. // Now check that sid_early is denied due to softlock.
@ -3173,7 +3279,11 @@ mod tests {
// Expect success // Expect success
let r2 = idms_auth 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; .await;
debug!("r2 ==> {:?}", r2); debug!("r2 ==> {:?}", r2);
match r2 { match r2 {

View file

@ -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, 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, FilterInvalid, FilterValid, FC,
}; };
pub use crate::idm::server::{IdmServer, IdmServerDelayed}; pub use crate::idm::server::{IdmServer, IdmServerAudit, IdmServerDelayed};
pub use crate::modify::{ pub use crate::modify::{
m_assert, m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList, ModifyValid, m_assert, m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList, ModifyValid,
}; };
pub use crate::server::access::AccessControlsTransaction; pub use crate::server::access::AccessControlsTransaction;
pub use crate::server::batch_modify::BatchModifyEvent; 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::{ pub use crate::server::{
QueryServer, QueryServerReadTransaction, QueryServerTransaction, QueryServer, QueryServerReadTransaction, QueryServerTransaction,
QueryServerWriteTransaction, QueryServerWriteTransaction,

View file

@ -6,6 +6,7 @@
use crate::be::Limits; use crate::be::Limits;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::hash::Hash; use std::hash::Hash;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use uuid::uuid; use uuid::uuid;
@ -16,6 +17,13 @@ use serde::{Deserialize, Serialize};
use crate::prelude::*; use crate::prelude::*;
use crate::value::Session; use crate::value::Session;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
Internal,
Https(IpAddr),
// Ldaps,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessScope { pub enum AccessScope {
ReadOnly, ReadOnly,

View file

@ -1037,8 +1037,6 @@ impl QueryServer {
let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap)); let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap));
// log_event!(log, "Starting query worker ...");
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
QueryServer { QueryServer {
phase, phase,

View file

@ -57,7 +57,7 @@ pub async fn setup_pair_test() -> (QueryServer, QueryServer) {
} }
#[allow(clippy::expect_used)] #[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; let qs = setup_test().await;
qs.initialise_helper(duration_from_epoch_now()) qs.initialise_helper(duration_from_epoch_now())