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::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -110,6 +111,7 @@ impl QueryServerReadV1 {
sessionid: Option<Uuid>,
req: AuthRequest,
eventid: Uuid,
ip_addr: IpAddr,
) -> Result<AuthResult, OperationError> {
// 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<String>,
issue: AuthIssueSession,
eventid: Uuid,
ip_addr: IpAddr,
) -> Result<AuthResult, OperationError> {
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));

View file

@ -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<JwsValidator>,
/// The SHA384 hashes of javascript files we're going to serve to users
pub js_files: Vec<JavaScriptFile>,
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<Uuid, tide::Error>;
fn new_eventid(&self) -> (Uuid, String);
fn get_remote_addr(&self) -> Option<IpAddr>;
}
impl RequestExtensions for tide::Request<AppState> {
@ -178,6 +182,16 @@ impl RequestExtensions for tide::Request<AppState> {
let hv = eventid.as_hyphenated().to_string();
(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>(
@ -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.

View file

@ -1068,6 +1068,14 @@ pub async fn reauth(mut req: tide::Request<AppState>) -> 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<AppState>) -> 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<AppState>) -> tide::Result {
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| {
debug!("Failed get body JSON? {:?}", e);
e
@ -1103,7 +1119,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> 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)

View file

@ -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,7 +651,8 @@ pub async fn create_server_core(
}
};
// 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,
Err(e) => {
error!("Unable to setup query server or idm server -> {:?}", e);
@ -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)

View file

@ -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)]
{

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::{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<Self>, 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<Self>, 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<DelayedAction>,
audit_tx: &Sender<AuditEvent>,
webauthn: &Webauthn,
pw_badlist_set: Option<&HashSet<String>>,
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());
}
}

View file

@ -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");

View file

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

View file

@ -23,6 +23,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
ident: Identity,
issue: AuthIssueSession,
ct: Duration,
source: Source,
) -> Result<AuthResult, OperationError> {
// 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)

View file

@ -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<DelayedAction>,
audit_tx: Sender<AuditEvent>,
/// [Webauthn] verifier/config
webauthn: Webauthn,
pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
@ -100,6 +102,7 @@ pub struct IdmServerAuthTransaction<'a> {
pub(crate) sid: Sid,
// For flagging eventual actions.
pub(crate) async_tx: Sender<DelayedAction>,
pub(crate) audit_tx: Sender<AuditEvent>,
pub(crate) webauthn: &'a Webauthn,
pub(crate) pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
@ -120,7 +123,6 @@ pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
// pub(crate) async_tx: Sender<DelayedAction>,
}
pub struct IdmServerProxyWriteTransaction<'a> {
@ -141,12 +143,15 @@ pub struct IdmServerDelayed {
pub(crate) async_rx: Receiver<DelayedAction>,
}
pub struct IdmServerAudit {
pub(crate) audit_rx: Receiver<AuditEvent>,
}
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<DelayedAction> {
self.async_rx.blocking_recv()
pub fn audit_rx(&mut self) -> &mut Receiver<AuditEvent> {
&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<DelayedAction, OperationError> {
@ -909,6 +932,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
&mut self,
ae: &AuthEvent,
ct: Duration,
source: Source,
) -> Result<AuthResult, OperationError> {
// 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 {

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,
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,

View file

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

View file

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

View file

@ -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())