use std::convert::TryFrom;
use std::sync::Arc;
use std::time::Duration;

use kanidm_lib_crypto::CryptoPolicy;

use compact_jwt::{Jwk, JwsCompact};
use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
use concread::cowcell::CowCellReadTxn;
use concread::hashmap::HashMap;
use kanidm_proto::internal::{
    ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
    UatPurpose, UserAuthToken,
};
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
use rand::prelude::*;
use tokio::sync::mpsc::{
    unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender,
};
use tokio::sync::{Mutex, Semaphore};
use tracing::trace;
use url::Url;
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};

use super::event::ReadBackupCodeEvent;
use super::ldap::{LdapBoundToken, LdapSession};
use crate::credential::{softlock::CredSoftLock, Credential};
use crate::idm::account::Account;
use crate::idm::application::{
    GenerateApplicationPasswordEvent, LdapApplications, LdapApplicationsReadTransaction,
    LdapApplicationsWriteTransaction,
};
use crate::idm::audit::AuditEvent;
use crate::idm::authsession::{AuthSession, AuthSessionData};
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
use crate::idm::delayed::{
    AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, UnixPasswordUpgrade,
    WebauthnCounterIncrement,
};

#[cfg(test)]
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{AuthEvent, AuthEventStep, AuthResult};
use crate::idm::event::{
    CredentialStatusEvent, LdapAuthEvent, LdapTokenAuthEvent, RadiusAuthTokenEvent,
    RegenerateRadiusSecretEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
    UnixUserTokenEvent,
};
use crate::idm::group::{Group, Unix};
use crate::idm::oauth2::{
    Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
    Oauth2ResourceServersWriteTransaction,
};
use crate::idm::radius::RadiusAccount;
use crate::idm::scim::SyncAccount;
use crate::idm::serviceaccount::ServiceAccount;
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyProvidersTransaction;
use crate::server::DomainInfo;
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
use crate::value::{Session, SessionState};

pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;

pub type DomainInfoRead = CowCellReadTxn<DomainInfo>;

pub struct IdmServer {
    // There is a good reason to keep this single thread - it
    // means that limits to sessions can be easily applied and checked to
    // various accounts, and we have a good idea of how to structure the
    // in memory caches related to locking.
    session_ticket: Semaphore,
    sessions: BptreeMap<Uuid, AuthSessionMutex>,
    softlocks: HashMap<Uuid, CredSoftLockMutex>,
    /// A set of in progress credential registrations
    cred_update_sessions: BptreeMap<Uuid, CredentialUpdateSessionMutex>,
    /// Reference to the query server.
    qs: QueryServer,
    /// 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,
    oauth2rs: Arc<Oauth2ResourceServers>,
    applications: Arc<LdapApplications>,
}

/// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication.
pub struct IdmServerAuthTransaction<'a> {
    pub(crate) session_ticket: &'a Semaphore,
    pub(crate) sessions: &'a BptreeMap<Uuid, AuthSessionMutex>,
    pub(crate) softlocks: &'a HashMap<Uuid, CredSoftLockMutex>,

    pub qs_read: QueryServerReadTransaction<'a>,
    /// Thread/Server ID
    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) applications: LdapApplicationsReadTransaction,
}

pub struct IdmServerCredUpdateTransaction<'a> {
    pub(crate) qs_read: QueryServerReadTransaction<'a>,
    // sid: Sid,
    pub(crate) webauthn: &'a Webauthn,
    pub(crate) cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>,
    pub(crate) crypto_policy: &'a CryptoPolicy,
}

/// This contains read-only methods, like getting users, groups and other structured content.
pub struct IdmServerProxyReadTransaction<'a> {
    pub qs_read: QueryServerReadTransaction<'a>,
    pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
}

pub struct IdmServerProxyWriteTransaction<'a> {
    // This does NOT take any read to the memory content, allowing safe
    // qs operations to occur through this interface.
    pub qs_write: QueryServerWriteTransaction<'a>,
    /// Associate to an event origin ID, which has a TS and a UUID instead
    pub(crate) cred_update_sessions: BptreeMapWriteTxn<'a, Uuid, CredentialUpdateSessionMutex>,
    pub(crate) sid: Sid,
    crypto_policy: &'a CryptoPolicy,
    webauthn: &'a Webauthn,
    pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
    pub(crate) applications: LdapApplicationsWriteTransaction<'a>,
}

pub struct IdmServerDelayed {
    pub(crate) async_rx: Receiver<DelayedAction>,
}

pub struct IdmServerAudit {
    pub(crate) audit_rx: Receiver<AuditEvent>,
}

impl IdmServer {
    pub async fn new(
        qs: QueryServer,
        origin: &str,
    ) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
        let crypto_policy = if cfg!(test) {
            CryptoPolicy::danger_test_minimum()
        } else {
            // This is calculated back from:
            //  100 password auths / thread -> 0.010 sec per op
            CryptoPolicy::time_target(Duration::from_millis(10))
        };

        let (async_tx, async_rx) = unbounded();
        let (audit_tx, audit_rx) = unbounded();

        // Get the domain name, as the relying party id.
        let (rp_id, rp_name, domain_level, oauth2rs_set, application_set) = {
            let mut qs_read = qs.read().await?;
            (
                qs_read.get_domain_name().to_string(),
                qs_read.get_domain_display_name().to_string(),
                qs_read.get_domain_version(),
                // Add a read/reload of all oauth2 configurations.
                qs_read.get_oauth2rs_set()?,
                qs_read.get_applications_set()?,
            )
        };

        // Check that it gels with our origin.
        let origin_url = Url::parse(origin)
            .map_err(|_e| {
                admin_error!("Unable to parse origin URL - refusing to start. You must correct the value for origin. {:?}", origin);
                OperationError::InvalidState
            })
            .and_then(|url| {
                let valid = url.domain().map(|effective_domain| {
                    // We need to prepend the '.' here to ensure that myexample.com != example.com,
                    // rather than just ends with.
                    effective_domain.ends_with(&format!(".{rp_id}"))
                    || effective_domain == rp_id
                }).unwrap_or(false);

                if valid {
                    Ok(url)
                } else {
                    admin_error!("Effective domain (ed) is not a descendent of server domain name (rp_id).");
                    admin_error!("You must change origin or domain name to be consistent. ded: {:?} - rp_id: {:?}", origin, rp_id);
                    admin_error!("To change the origin or domain name see: https://kanidm.github.io/kanidm/master/server_configuration.html");
                    Err(OperationError::InvalidState)
                }
            })?;

        let webauthn = WebauthnBuilder::new(&rp_id, &origin_url)
            .and_then(|builder| builder.allow_subdomains(true).rp_name(&rp_name).build())
            .map_err(|e| {
                admin_error!("Invalid Webauthn Configuration - {:?}", e);
                OperationError::InvalidState
            })?;

        let oauth2rs = Oauth2ResourceServers::try_from((oauth2rs_set, origin_url, domain_level))
            .map_err(|e| {
                admin_error!("Failed to load oauth2 resource servers - {:?}", e);
                e
            })?;

        let applications = LdapApplications::try_from(application_set).map_err(|e| {
            admin_error!("Failed to load ldap applications - {:?}", e);
            e
        })?;

        Ok((
            IdmServer {
                session_ticket: Semaphore::new(1),
                sessions: BptreeMap::new(),
                softlocks: HashMap::new(),
                cred_update_sessions: BptreeMap::new(),
                qs,
                crypto_policy,
                async_tx,
                audit_tx,
                webauthn,
                oauth2rs: Arc::new(oauth2rs),
                applications: Arc::new(applications),
            },
            IdmServerDelayed { async_rx },
            IdmServerAudit { audit_rx },
        ))
    }

    /// Start an auth txn
    pub async fn auth(&self) -> Result<IdmServerAuthTransaction<'_>, OperationError> {
        let qs_read = self.qs.read().await?;

        let mut sid = [0; 4];
        let mut rng = StdRng::from_entropy();
        rng.fill(&mut sid);

        Ok(IdmServerAuthTransaction {
            session_ticket: &self.session_ticket,
            sessions: &self.sessions,
            softlocks: &self.softlocks,
            qs_read,
            sid,
            async_tx: self.async_tx.clone(),
            audit_tx: self.audit_tx.clone(),
            webauthn: &self.webauthn,
            applications: self.applications.read(),
        })
    }

    /// Begin a fast (low cost) read of the servers domain info. It is important to note
    /// this does not conflict with any other type of transaction type and may safely
    /// beheld over other transaction boundaries.
    #[instrument(level = "debug", skip_all)]
    pub fn domain_read(&self) -> DomainInfoRead {
        self.qs.d_info.read()
    }

    /// Read from the database, in a transaction.
    #[instrument(level = "debug", skip_all)]
    pub async fn proxy_read(&self) -> Result<IdmServerProxyReadTransaction<'_>, OperationError> {
        let qs_read = self.qs.read().await?;
        Ok(IdmServerProxyReadTransaction {
            qs_read,
            oauth2rs: self.oauth2rs.read(),
            // async_tx: self.async_tx.clone(),
        })
    }

    #[instrument(level = "debug", skip_all)]
    pub async fn proxy_write(
        &self,
        ts: Duration,
    ) -> Result<IdmServerProxyWriteTransaction<'_>, OperationError> {
        let qs_write = self.qs.write(ts).await?;

        let mut sid = [0; 4];
        let mut rng = StdRng::from_entropy();
        rng.fill(&mut sid);

        Ok(IdmServerProxyWriteTransaction {
            cred_update_sessions: self.cred_update_sessions.write(),
            qs_write,
            sid,
            crypto_policy: &self.crypto_policy,
            webauthn: &self.webauthn,
            oauth2rs: self.oauth2rs.write(),
            applications: self.applications.write(),
        })
    }

    pub async fn cred_update_transaction(
        &self,
    ) -> Result<IdmServerCredUpdateTransaction<'_>, OperationError> {
        let qs_read = self.qs.read().await?;
        Ok(IdmServerCredUpdateTransaction {
            qs_read,
            // sid: Sid,
            webauthn: &self.webauthn,
            cred_update_sessions: self.cred_update_sessions.read(),
            crypto_policy: &self.crypto_policy,
        })
    }

    #[cfg(test)]
    pub(crate) async fn delayed_action(
        &self,
        ct: Duration,
        da: DelayedAction,
    ) -> Result<bool, OperationError> {
        let mut pw = self.proxy_write(ct).await?;
        pw.process_delayedaction(&da, ct)
            .and_then(|_| pw.commit())
            .map(|()| true)
    }
}

impl IdmServerAudit {
    #[cfg(test)]
    pub(crate) fn check_is_empty_or_panic(&mut self) {
        use tokio::sync::mpsc::error::TryRecvError;

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

    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");
            }
            #[allow(clippy::panic)]
            Ok(m) => {
                trace!(?m);
                panic!("Task queue not empty");
            }
        }
    }

    #[cfg(test)]
    pub(crate) fn try_recv(&mut self) -> Result<DelayedAction, OperationError> {
        use core::task::{Context, Poll};
        use futures::task as futures_task;

        let waker = futures_task::noop_waker();
        let mut cx = Context::from_waker(&waker);
        match self.async_rx.poll_recv(&mut cx) {
            Poll::Pending => Err(OperationError::InvalidState),
            Poll::Ready(None) => Err(OperationError::QueueDisconnected),
            Poll::Ready(Some(m)) => Ok(m),
        }
    }

    pub async fn recv_many(&mut self, buffer: &mut Vec<DelayedAction>) -> usize {
        debug_assert!(buffer.is_empty());
        let limit = buffer.capacity();
        self.async_rx.recv_many(buffer, limit).await
    }
}

pub enum Token {
    UserAuthToken(UserAuthToken),
    ApiToken(ApiToken, Arc<EntrySealedCommitted>),
}

pub trait IdmServerTransaction<'a> {
    type QsTransactionType: QueryServerTransaction<'a>;

    fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType;

    /// This is the preferred method to transform and securely verify a token into
    /// an identity that can be used for operations and access enforcement. This
    /// function *is* aware of the various classes of tokens that may exist, and can
    /// appropriately check them.
    ///
    /// The primary method of verification selection is the use of the KID parameter
    /// that we internally sign with. We can use this to select the appropriate token type
    /// and validation method.
    #[instrument(level = "info", skip_all)]
    fn validate_client_auth_info_to_ident(
        &mut self,
        client_auth_info: ClientAuthInfo,
        ct: Duration,
    ) -> Result<Identity, OperationError> {
        let ClientAuthInfo {
            source,
            client_cert,
            bearer_token,
            basic_authz: _,
        } = client_auth_info;

        match (client_cert, bearer_token) {
            (Some(client_cert_info), _) => {
                self.client_certificate_to_identity(&client_cert_info, ct, source)
            }
            (None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
                Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct, source),
                Token::ApiToken(apit, entry) => {
                    self.process_apit_to_identity(&apit, source, entry, ct)
                }
            },
            (None, None) => {
                debug!("No client certificate or bearer tokens were supplied");
                Err(OperationError::NotAuthenticated)
            }
        }
    }

    /// This function is not using in authentication flows - it is a reflector of the
    /// current session state to allow a user-auth-token to be presented to the
    /// user via the whoami call.
    #[instrument(level = "info", skip_all)]
    fn validate_client_auth_info_to_uat(
        &mut self,
        client_auth_info: ClientAuthInfo,
        ct: Duration,
    ) -> Result<UserAuthToken, OperationError> {
        let ClientAuthInfo {
            client_cert,
            bearer_token,
            source: _,
            basic_authz: _,
        } = client_auth_info;

        match (client_cert, bearer_token) {
            (Some(client_cert_info), _) => {
                self.client_certificate_to_user_auth_token(&client_cert_info, ct)
            }
            (None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
                Token::UserAuthToken(uat) => Ok(uat),
                Token::ApiToken(_apit, _entry) => {
                    warn!("Unable to process non user auth token");
                    Err(OperationError::NotAuthenticated)
                }
            },
            (None, None) => {
                debug!("No client certificate or bearer tokens were supplied");
                Err(OperationError::NotAuthenticated)
            }
        }
    }

    fn validate_and_parse_token_to_token(
        &mut self,
        jwsu: &JwsCompact,
        ct: Duration,
    ) -> Result<Token, OperationError> {
        // Our key objects now handle this logic and determine the correct key
        // from the input type.
        let jws_inner = self
            .get_qs_txn()
            .get_domain_key_object_handle()?
            .jws_verify(jwsu)
            .map_err(|err| {
                security_info!(?err, "Unable to verify token");
                OperationError::NotAuthenticated
            })?;

        // Is it a UAT?
        if let Ok(uat) = jws_inner.from_json::<UserAuthToken>() {
            if let Some(exp) = uat.expiry {
                let ct_odt = time::OffsetDateTime::UNIX_EPOCH + ct;
                if exp < ct_odt {
                    security_info!(?ct_odt, ?exp, "Session expired");
                    return Err(OperationError::SessionExpired);
                } else {
                    trace!(?ct_odt, ?exp, "Session not yet expired");
                    return Ok(Token::UserAuthToken(uat));
                }
            } else {
                debug!("Session has no expiry");
                return Ok(Token::UserAuthToken(uat));
            }
        };

        // Is it an API Token?
        if let Ok(apit) = jws_inner.from_json::<ApiToken>() {
            if let Some(expiry) = apit.expiry {
                if time::OffsetDateTime::UNIX_EPOCH + ct >= expiry {
                    security_info!("Session expired");
                    return Err(OperationError::SessionExpired);
                }
            }

            let entry = self
                .get_qs_txn()
                .internal_search_uuid(apit.account_id)
                .map_err(|err| {
                    security_info!(?err, "Account associated with api token no longer exists.");
                    OperationError::NotAuthenticated
                })?;

            return Ok(Token::ApiToken(apit, entry));
        };

        security_info!("Unable to verify token, invalid inner JSON");
        Err(OperationError::NotAuthenticated)
    }

    fn check_oauth2_account_uuid_valid(
        &mut self,
        uuid: Uuid,
        session_id: Uuid,
        parent_session_id: Option<Uuid>,
        iat: i64,
        ct: Duration,
    ) -> Result<Option<Arc<Entry<EntrySealed, EntryCommitted>>>, OperationError> {
        let entry = self.get_qs_txn().internal_search_uuid(uuid).map_err(|e| {
            admin_error!(?e, "check_oauth2_account_uuid_valid failed");
            e
        })?;

        let within_valid_window = Account::check_within_valid_time(
            ct,
            entry
                .get_ava_single_datetime(Attribute::AccountValidFrom)
                .as_ref(),
            entry
                .get_ava_single_datetime(Attribute::AccountExpire)
                .as_ref(),
        );

        if !within_valid_window {
            security_info!("Account has expired or is not yet valid, not allowing to proceed");
            return Ok(None);
        }

        // We are past the grace window. Enforce session presence.
        // We enforce both sessions are present in case of inconsistency
        // that may occur with replication.

        let grace_valid = ct < (Duration::from_secs(iat as u64) + AUTH_TOKEN_GRACE_WINDOW);

        let oauth2_session = entry
            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
            .and_then(|sessions| sessions.get(&session_id));

        if let Some(oauth2_session) = oauth2_session {
            // We have the oauth2 session, lets check it.
            let oauth2_session_valid = !matches!(oauth2_session.state, SessionState::RevokedAt(_));

            if !oauth2_session_valid {
                security_info!("The oauth2 session associated to this token is revoked.");
                return Ok(None);
            }

            // Do we have a parent session? If yes, we need to enforce it's presence.
            if let Some(parent_session_id) = parent_session_id {
                let uat_session = entry
                    .get_ava_as_session_map(Attribute::UserAuthTokenSession)
                    .and_then(|sessions| sessions.get(&parent_session_id));

                if let Some(uat_session) = uat_session {
                    let parent_session_valid =
                        !matches!(uat_session.state, SessionState::RevokedAt(_));
                    if parent_session_valid {
                        security_info!(
                            "A valid parent and oauth2 session value exists for this token"
                        );
                    } else {
                        security_info!(
                            "The parent oauth2 session associated to this token is revoked."
                        );
                        return Ok(None);
                    }
                } else if grace_valid {
                    security_info!(
                        "The token grace window is in effect. Assuming parent session valid."
                    );
                } else {
                    security_info!("The token grace window has passed and no entry parent sessions exist. Assuming invalid.");
                    return Ok(None);
                }
            }
            // If we don't have a parent session id, we are good to proceed.
        } else if grace_valid {
            security_info!("The token grace window is in effect. Assuming valid.");
        } else {
            security_info!(
                "The token grace window has passed and no entry sessions exist. Assuming invalid."
            );
            return Ok(None);
        }

        Ok(Some(entry))
    }

    /// For any event/operation to proceed, we need to attach an identity to the
    /// event for security and access processing. When that event is externally
    /// triggered via one of our various api layers, we process some type of
    /// account token into this identity. In the current server this is the
    /// UserAuthToken. For a UserAuthToken to be provided it MUST have been
    /// cryptographically verified meaning it is now a *trusted* source of
    /// data that we previously issued.
    ///
    /// This is the function that is responsible for converting that UAT into
    /// something we can pin access controls and other limits and references to.
    /// This is why it is the location where validity windows are checked and other
    /// relevant session information is injected.
    #[instrument(level = "debug", skip_all)]
    fn process_uat_to_identity(
        &mut self,
        uat: &UserAuthToken,
        ct: Duration,
        source: Source,
    ) -> Result<Identity, OperationError> {
        // From a UAT, get the current identity and associated information.
        let entry = self
            .get_qs_txn()
            .internal_search_uuid(uat.uuid)
            .map_err(|e| {
                admin_error!(?e, "from_ro_uat failed");
                e
            })?;

        let valid = Account::check_user_auth_token_valid(ct, uat, &entry);

        if !valid {
            return Err(OperationError::SessionExpired);
        }

        // ✅  Session is valid! Start to setup for it to be used.

        let scope = match uat.purpose {
            UatPurpose::ReadOnly => AccessScope::ReadOnly,
            UatPurpose::ReadWrite { expiry: None } => AccessScope::ReadOnly,
            UatPurpose::ReadWrite {
                expiry: Some(expiry),
            } => {
                let cot = time::OffsetDateTime::UNIX_EPOCH + ct;
                if cot < expiry {
                    AccessScope::ReadWrite
                } else {
                    AccessScope::ReadOnly
                }
            }
        };

        let mut limits = Limits::default();
        // Apply the limits from the uat
        if let Some(lim) = uat.limit_search_max_results.and_then(|v| v.try_into().ok()) {
            limits.search_max_results = lim;
        }
        if let Some(lim) = uat
            .limit_search_max_filter_test
            .and_then(|v| v.try_into().ok())
        {
            limits.search_max_filter_test = lim;
        }

        // #64: Now apply claims from the uat into the Entry
        // to allow filtering.
        /*
        entry.insert_claim(match &uat.auth_type {
            AuthType::Anonymous => "authtype_anonymous",
            AuthType::UnixPassword => "authtype_unixpassword",
            AuthType::Password => "authtype_password",
            AuthType::GeneratedPassword => "authtype_generatedpassword",
            AuthType::Webauthn => "authtype_webauthn",
            AuthType::PasswordMfa => "authtype_passwordmfa",
        });

        trace!(claims = ?entry.get_ava_set("claim"), "Applied claims");
        */

        Ok(Identity::new(
            IdentType::User(IdentUser { entry }),
            source,
            uat.session_id,
            scope,
            limits,
        ))
    }

    #[instrument(level = "debug", skip_all)]
    fn process_apit_to_identity(
        &mut self,
        apit: &ApiToken,
        source: Source,
        entry: Arc<EntrySealedCommitted>,
        ct: Duration,
    ) -> Result<Identity, OperationError> {
        let valid = ServiceAccount::check_api_token_valid(ct, apit, &entry);

        if !valid {
            // Check_api token logs this.
            return Err(OperationError::SessionExpired);
        }

        let scope = (&apit.purpose).into();

        let limits = Limits::api_token();
        Ok(Identity::new(
            IdentType::User(IdentUser { entry }),
            source,
            apit.token_id,
            scope,
            limits,
        ))
    }

    fn client_cert_info_entry(
        &mut self,
        client_cert_info: &ClientCertInfo,
    ) -> Result<Arc<EntrySealedCommitted>, OperationError> {
        let pks256 = hex::encode(client_cert_info.public_key_s256);
        // Using the certificate hash, find our matching cert.
        let mut maybe_cert_entries = self.get_qs_txn().internal_search(filter!(f_eq(
            Attribute::Certificate,
            PartialValue::HexString(pks256.clone())
        )))?;

        let maybe_cert_entry = maybe_cert_entries.pop();

        if let Some(cert_entry) = maybe_cert_entry {
            if maybe_cert_entries.is_empty() {
                Ok(cert_entry)
            } else {
                debug!(?pks256, "Multiple certificates matched, unable to proceed.");
                Err(OperationError::NotAuthenticated)
            }
        } else {
            debug!(?pks256, "No certificates were able to be mapped.");
            Err(OperationError::NotAuthenticated)
        }
    }

    /// Given a certificate, validate it and discover the associated entry that
    /// the certificate relates to. Currently, this relies on mapping the public
    /// key sha256 to a stored client certificate, which then links to the owner.
    ///
    /// In the future we *could* consider alternate mapping strategies such as
    /// subjectAltName or subject DN, but these have subtle security risks and
    /// configuration challenges, so binary mapping is the simplest - and safest -
    /// option today.
    #[instrument(level = "debug", skip_all)]
    fn client_certificate_to_identity(
        &mut self,
        client_cert_info: &ClientCertInfo,
        ct: Duration,
        source: Source,
    ) -> Result<Identity, OperationError> {
        let cert_entry = self.client_cert_info_entry(client_cert_info)?;

        // This is who the certificate belongs to.
        let refers_uuid = cert_entry
            .get_ava_single_refer(Attribute::Refers)
            .ok_or_else(|| {
                warn!("Invalid certificate entry, missing refers");
                OperationError::InvalidState
            })?;

        // Now get the related entry.
        let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;

        let (account, account_policy) =
            Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;

        // Is the account in it's valid window?
        if !account.is_within_valid_time(ct) {
            // Nope, expired
            return Err(OperationError::SessionExpired);
        };

        // scope is related to the cert. For now, default to RO.
        let scope = AccessScope::ReadOnly;

        let mut limits = Limits::default();
        // Apply the limits from the account policy
        if let Some(lim) = account_policy
            .limit_search_max_results()
            .and_then(|v| v.try_into().ok())
        {
            limits.search_max_results = lim;
        }
        if let Some(lim) = account_policy
            .limit_search_max_filter_test()
            .and_then(|v| v.try_into().ok())
        {
            limits.search_max_filter_test = lim;
        }

        let certificate_uuid = cert_entry.get_uuid();

        Ok(Identity::new(
            IdentType::User(IdentUser { entry }),
            source,
            // session_id is the certificate uuid.
            certificate_uuid,
            scope,
            limits,
        ))
    }

    #[instrument(level = "debug", skip_all)]
    fn client_certificate_to_user_auth_token(
        &mut self,
        client_cert_info: &ClientCertInfo,
        ct: Duration,
    ) -> Result<UserAuthToken, OperationError> {
        let cert_entry = self.client_cert_info_entry(client_cert_info)?;

        // This is who the certificate belongs to.
        let refers_uuid = cert_entry
            .get_ava_single_refer(Attribute::Refers)
            .ok_or_else(|| {
                warn!("Invalid certificate entry, missing refers");
                OperationError::InvalidState
            })?;

        // Now get the related entry.
        let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;

        let (account, account_policy) =
            Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;

        // Is the account in it's valid window?
        if !account.is_within_valid_time(ct) {
            // Nope, expired
            return Err(OperationError::SessionExpired);
        };

        let certificate_uuid = cert_entry.get_uuid();
        let session_is_rw = false;

        account
            .client_cert_info_to_userauthtoken(certificate_uuid, session_is_rw, ct, &account_policy)
            .ok_or(OperationError::InvalidState)
    }

    fn process_ldap_uuid_to_identity(
        &mut self,
        uuid: &Uuid,
        ct: Duration,
        source: Source,
    ) -> Result<Identity, OperationError> {
        let entry = self
            .get_qs_txn()
            .internal_search_uuid(*uuid)
            .map_err(|err| {
                error!(?err, ?uuid, "Failed to search user by uuid");
                err
            })?;

        let (account, account_policy) =
            Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;

        if !account.is_within_valid_time(ct) {
            info!("Account is expired or not yet valid.");
            return Err(OperationError::SessionExpired);
        }

        // Good to go
        let anon_entry = if *uuid == UUID_ANONYMOUS {
            // We already have it.
            entry
        } else {
            // Pull the anon entry for mapping the identity.
            self.get_qs_txn()
                .internal_search_uuid(UUID_ANONYMOUS)
                .map_err(|err| {
                    error!(
                        ?err,
                        "Unable to search anonymous user for privilege bounding."
                    );
                    err
                })?
        };

        let mut limits = Limits::default();
        let session_id = Uuid::new_v4();

        // Update limits from account policy
        if let Some(max_results) = account_policy.limit_search_max_results() {
            limits.search_max_results = max_results as usize;
        }
        if let Some(max_filter) = account_policy.limit_search_max_filter_test() {
            limits.search_max_filter_test = max_filter as usize;
        }

        // Users via LDAP are always only granted anonymous rights unless
        // they auth with an api-token
        Ok(Identity::new(
            IdentType::User(IdentUser { entry: anon_entry }),
            source,
            session_id,
            AccessScope::ReadOnly,
            limits,
        ))
    }

    #[instrument(level = "debug", skip_all)]
    fn validate_ldap_session(
        &mut self,
        session: &LdapSession,
        source: Source,
        ct: Duration,
    ) -> Result<Identity, OperationError> {
        match session {
            LdapSession::UnixBind(uuid) | LdapSession::ApplicationPasswordBind(_, uuid) => {
                self.process_ldap_uuid_to_identity(uuid, ct, source)
            }
            LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(uat, ct, source),
            LdapSession::ApiToken(apit) => {
                let entry = self
                    .get_qs_txn()
                    .internal_search_uuid(apit.account_id)
                    .map_err(|e| {
                        admin_error!("Failed to validate ldap session -> {:?}", e);
                        e
                    })?;

                self.process_apit_to_identity(apit, source, entry, ct)
            }
        }
    }

    #[instrument(level = "info", skip_all)]
    fn validate_sync_client_auth_info_to_ident(
        &mut self,
        client_auth_info: ClientAuthInfo,
        ct: Duration,
    ) -> Result<Identity, OperationError> {
        // FUTURE: Could allow mTLS here instead?

        let jwsu = client_auth_info.bearer_token.ok_or_else(|| {
            security_info!("No token provided");
            OperationError::NotAuthenticated
        })?;

        let jws_inner = self
            .get_qs_txn()
            .get_domain_key_object_handle()?
            .jws_verify(&jwsu)
            .map_err(|err| {
                security_info!(?err, "Unable to verify token");
                OperationError::NotAuthenticated
            })?;

        let sync_token = jws_inner.from_json::<ScimSyncToken>().map_err(|err| {
            error!(?err, "Unable to deserialise JWS");
            OperationError::SerdeJsonError
        })?;

        let entry = self
            .get_qs_txn()
            .internal_search(filter!(f_eq(
                Attribute::SyncTokenSession,
                PartialValue::Refer(sync_token.token_id)
            )))
            .and_then(|mut vs| match vs.pop() {
                Some(entry) if vs.is_empty() => Ok(entry),
                _ => {
                    admin_error!(
                        token_id = ?sync_token.token_id,
                        "entries was empty, or matched multiple results for token id"
                    );
                    Err(OperationError::NotAuthenticated)
                }
            })?;

        let valid = SyncAccount::check_sync_token_valid(ct, &sync_token, &entry);

        if !valid {
            security_info!("Unable to proceed with invalid sync token");
            return Err(OperationError::NotAuthenticated);
        }

        // If scope is not Synchronise, then fail.
        let scope = (&sync_token.purpose).into();

        let limits = Limits::unlimited();
        Ok(Identity::new(
            IdentType::Synch(entry.get_uuid()),
            client_auth_info.source,
            sync_token.token_id,
            scope,
            limits,
        ))
    }
}

impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> {
    type QsTransactionType = QueryServerReadTransaction<'a>;

    fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
        &mut self.qs_read
    }
}

impl<'a> IdmServerAuthTransaction<'a> {
    #[cfg(test)]
    pub fn is_sessionid_present(&self, sessionid: Uuid) -> bool {
        let session_read = self.sessions.read();
        session_read.contains_key(&sessionid)
    }

    pub fn get_origin(&self) -> &Url {
        #[allow(clippy::unwrap_used)]
        self.webauthn.get_allowed_origins().first().unwrap()
    }

    #[instrument(level = "trace", skip(self))]
    pub async fn expire_auth_sessions(&mut self, ct: Duration) {
        // ct is current time - sub the timeout. and then split.
        let expire = ct - Duration::from_secs(AUTH_SESSION_TIMEOUT);
        let split_at = uuid_from_duration(expire, self.sid);
        // Removes older sessions in place.
        let _session_ticket = self.session_ticket.acquire().await;
        let mut session_write = self.sessions.write();
        session_write.split_off_lt(&split_at);
        // expired will now be dropped, and can't be used by future sessions.
        session_write.commit();
    }

    pub async fn auth(
        &mut self,
        ae: &AuthEvent,
        ct: Duration,
        client_auth_info: ClientAuthInfo,
    ) -> Result<AuthResult, OperationError> {
        // Match on the auth event, to see what we need to do.
        match &ae.step {
            AuthEventStep::Init(init) => {
                // lperf_segment!("idm::server::auth<Init>", || {
                // Allocate a session id, based on current time.
                let sessionid = uuid_from_duration(ct, self.sid);

                // Begin the auth procedure!
                // Start a read
                //
                // Actually we may not need this - at the time we issue the auth-init
                // we could generate the uat, the nonce and cache hashes in memory,
                // then this can just be fully without a txn.
                //
                // We do need a txn so that we can process/search and claims
                // or related based on the quality of the provided auth steps
                //
                // We *DO NOT* need a write though, because I think that lock outs
                // and rate limits are *per server* and *in memory* only.
                //
                // Check anything needed? Get the current auth-session-id from request
                // because it associates to the nonce's etc which were all cached.
                let euuid = self.qs_read.name_to_uuid(init.username.as_str())?;

                // Get the first / single entry we expect here ....
                let entry = self.qs_read.internal_search_uuid(euuid)?;

                security_info!(
                    username = %init.username,
                    issue = ?init.issue,
                    privileged = ?init.privileged,
                    uuid = %euuid,
                    "Initiating Authentication Session",
                );

                // Now, convert the Entry to an account - this gives us some stronger
                // typing and functionality so we can assess what auth types can
                // continue, and helps to keep non-needed entry specific data
                // out of the session tree.
                let (account, account_policy) =
                    Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;

                trace!(?account.primary);

                // Intent to take both trees to write.
                let _session_ticket = self.session_ticket.acquire().await;

                // We don't actually check the softlock here. We just initialise
                // it under the write lock we currently have, so that we can validate
                // it once we understand what auth mech we will be using.
                //
                // NOTE: Very careful use of await here to avoid an issue with write.
                let _maybe_slock_ref =
                    account
                        .primary_cred_uuid_and_policy()
                        .map(|(cred_uuid, policy)| {
                            // Acquire the softlock map
                            //
                            // We have no issue calling this with .write here, since we
                            // already hold the session_ticket above.
                            let mut softlock_write = self.softlocks.write();
                            let slock_ref: CredSoftLockMutex =
                                if let Some(slock_ref) = softlock_write.get(&cred_uuid) {
                                    slock_ref.clone()
                                } else {
                                    // Create if not exist, and the cred type supports softlocking.
                                    let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
                                    softlock_write.insert(cred_uuid, slock.clone());
                                    slock
                                };
                            softlock_write.commit();
                            slock_ref
                        });

                let asd: AuthSessionData = AuthSessionData {
                    account,
                    account_policy,
                    issue: init.issue,
                    webauthn: self.webauthn,
                    ct,
                    client_auth_info,
                };

                let domain_keys = self.qs_read.get_domain_key_object_handle()?;

                let (auth_session, state) = AuthSession::new(asd, init.privileged, domain_keys);

                match auth_session {
                    Some(auth_session) => {
                        let mut session_write = self.sessions.write();
                        if session_write.contains_key(&sessionid) {
                            // If we have a session of the same id, return an error (despite how
                            // unlikely this is ...
                            Err(OperationError::InvalidSessionState)
                        } else {
                            session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
                            // Debugging: ensure we really inserted ...
                            debug_assert!(session_write.get(&sessionid).is_some());
                            Ok(())
                        }?;
                        session_write.commit();
                    }
                    None => {
                        security_info!("Authentication Session Unable to begin");
                    }
                };

                Ok(AuthResult { sessionid, state })
            } // AuthEventStep::Init
            AuthEventStep::Begin(mech) => {
                let session_read = self.sessions.read();
                // Do we have a session?
                let auth_session_ref = session_read
                    // Why is the session missing?
                    .get(&mech.sessionid)
                    .cloned()
                    .ok_or_else(|| {
                        admin_error!("Invalid Session State (no present session uuid)");
                        OperationError::InvalidSessionState
                    })?;

                let mut auth_session = auth_session_ref.lock().await;

                // Indicate to the session which auth mech we now want to proceed with.
                let auth_result = auth_session.start_session(&mech.mech);

                let is_valid = match auth_session.get_credential_uuid()? {
                    Some(cred_uuid) => {
                        // From the auth_session, determine if the current account
                        // credential that we are using has become softlocked or not.
                        let softlock_read = self.softlocks.read();
                        if let Some(slock_ref) = softlock_read.get(&cred_uuid) {
                            let mut slock = slock_ref.lock().await;
                            // Apply the current time.
                            slock.apply_time_step(ct);
                            // Now check the results
                            slock.is_valid()
                        } else {
                            trace!("slock not found");
                            false
                        }
                    }
                    None => true,
                };

                if is_valid {
                    auth_result
                } else {
                    // Fail the session
                    trace!("lock step begin");
                    auth_session.end_session("Account is temporarily locked")
                }
                .map(|aus| AuthResult {
                    sessionid: mech.sessionid,
                    state: aus,
                })
            } // End AuthEventStep::Mech
            AuthEventStep::Cred(creds) => {
                // lperf_segment!("idm::server::auth<Creds>", || {
                // let _session_ticket = self.session_ticket.acquire().await;

                let session_read = self.sessions.read();
                // Do we have a session?
                let auth_session_ref = session_read
                    // Why is the session missing?
                    .get(&creds.sessionid)
                    .cloned()
                    .ok_or_else(|| {
                        admin_error!("Invalid Session State (no present session uuid)");
                        OperationError::InvalidSessionState
                    })?;

                let mut auth_session = auth_session_ref.lock().await;

                let maybe_slock_ref = match auth_session.get_credential_uuid()? {
                    Some(cred_uuid) => {
                        let softlock_read = self.softlocks.read();
                        softlock_read.get(&cred_uuid).cloned()
                    }
                    None => None,
                };

                // From the auth_session, determine if the current account
                // credential that we are using has become softlocked or not.
                let mut maybe_slock = if let Some(s) = maybe_slock_ref.as_ref() {
                    Some(s.lock().await)
                } else {
                    None
                };

                let is_valid = if let Some(ref mut slock) = maybe_slock {
                    // Apply the current time.
                    slock.apply_time_step(ct);
                    // Now check the results
                    slock.is_valid()
                } else {
                    // No slock is present for this cred_uuid
                    true
                };

                if is_valid {
                    // Process the credentials here as required.
                    // Basically throw them at the auth_session and see what
                    // falls out.
                    auth_session
                        .validate_creds(
                            &creds.cred,
                            ct,
                            &self.async_tx,
                            &self.audit_tx,
                            self.webauthn,
                            self.qs_read.pw_badlist(),
                        )
                        .inspect(|aus| {
                            // Inspect the result:
                            // if it was a failure, we need to inc the softlock.
                            if let AuthState::Denied(_) = aus {
                                // Update it.
                                if let Some(ref mut slock) = maybe_slock {
                                    slock.record_failure(ct);
                                }
                            };
                        })
                } else {
                    // Fail the session
                    trace!("lock step cred");
                    auth_session.end_session("Account is temporarily locked")
                }
                .map(|aus| AuthResult {
                    sessionid: creds.sessionid,
                    state: aus,
                })
            } // End AuthEventStep::Cred
        }
    }

    async fn auth_with_unix_pass(
        &mut self,
        id: Uuid,
        cleartext: &str,
        ct: Duration,
    ) -> Result<Option<Account>, OperationError> {
        let entry = match self.qs_read.internal_search_uuid(id) {
            Ok(entry) => entry,
            Err(e) => {
                admin_error!("Failed to start auth unix -> {:?}", e);
                return Err(e);
            }
        };

        let (account, acp) =
            Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;

        if !account.is_within_valid_time(ct) {
            security_info!("Account is expired or not yet valid.");
            return Ok(None);
        }

        let cred = if acp.allow_primary_cred_fallback() == Some(true) {
            account
                .unix_extn()
                .and_then(|extn| extn.ucred())
                .or_else(|| account.primary())
        } else {
            account.unix_extn().and_then(|extn| extn.ucred())
        };

        let (cred, cred_id, cred_slock_policy) = match cred {
            None => {
                if acp.allow_primary_cred_fallback() == Some(true) {
                    security_info!("Account does not have a POSIX or primary password configured.");
                } else {
                    security_info!("Account does not have a POSIX password configured.");
                }
                return Ok(None);
            }
            Some(cred) => (cred, cred.uuid, cred.softlock_policy()),
        };

        // The credential should only ever be a password
        let Ok(password) = cred.password_ref() else {
            error!("User's UNIX or primary credential is not a password, can't authenticate!");
            return Err(OperationError::InvalidState);
        };

        let slock_ref = {
            let softlock_read = self.softlocks.read();
            if let Some(slock_ref) = softlock_read.get(&cred_id) {
                slock_ref.clone()
            } else {
                let _session_ticket = self.session_ticket.acquire().await;
                let mut softlock_write = self.softlocks.write();
                let slock = Arc::new(Mutex::new(CredSoftLock::new(cred_slock_policy)));
                softlock_write.insert(cred_id, slock.clone());
                softlock_write.commit();
                slock
            }
        };

        let mut slock = slock_ref.lock().await;

        slock.apply_time_step(ct);

        if !slock.is_valid() {
            security_info!("Account is softlocked.");
            return Ok(None);
        }

        // Check the provided password against the stored hash
        let valid = password.verify(cleartext).map_err(|e| {
            error!(crypto_err = ?e);
            e.into()
        })?;

        if !valid {
            // Update it.
            slock.record_failure(ct);

            return Ok(None);
        }

        security_info!("Successfully authenticated with unix (or primary) password");
        if password.requires_upgrade() {
            self.async_tx
                .send(DelayedAction::UnixPwUpgrade(UnixPasswordUpgrade {
                    target_uuid: id,
                    existing_password: cleartext.to_string(),
                }))
                .map_err(|_| {
                    admin_error!("failed to queue delayed action - unix password upgrade");
                    OperationError::InvalidState
                })?;
        }

        Ok(Some(account))
    }

    pub async fn auth_unix(
        &mut self,
        uae: &UnixUserAuthEvent,
        ct: Duration,
    ) -> Result<Option<UnixUserToken>, OperationError> {
        Ok(self
            .auth_with_unix_pass(uae.target, &uae.cleartext, ct)
            .await?
            .and_then(|acc| acc.to_unixusertoken(ct).ok()))
    }

    pub async fn auth_ldap(
        &mut self,
        lae: &LdapAuthEvent,
        ct: Duration,
    ) -> Result<Option<LdapBoundToken>, OperationError> {
        if lae.target == UUID_ANONYMOUS {
            let account_entry = self.qs_read.internal_search_uuid(lae.target).map_err(|e| {
                admin_error!("Failed to start auth ldap -> {:?}", e);
                e
            })?;

            let account = Account::try_from_entry_ro(account_entry.as_ref(), &mut self.qs_read)?;

            // Check if the anon account has been locked.
            if !account.is_within_valid_time(ct) {
                security_info!("Account is not within valid time period");
                return Ok(None);
            }

            let session_id = Uuid::new_v4();
            security_info!(
                "Starting session {} for {} {}",
                session_id,
                account.spn,
                account.uuid
            );

            // Account must be anon, so we can gen the uat.
            Ok(Some(LdapBoundToken {
                session_id,
                spn: account.spn,
                effective_session: LdapSession::UnixBind(UUID_ANONYMOUS),
            }))
        } else {
            if !self.qs_read.d_info.d_ldap_allow_unix_pw_bind {
                security_info!("Bind not allowed through Unix passwords.");
                return Ok(None);
            }

            let auth = self
                .auth_with_unix_pass(lae.target, &lae.cleartext, ct)
                .await?;

            match auth {
                Some(account) => {
                    let session_id = Uuid::new_v4();
                    security_info!(
                        "Starting session {} for {} {}",
                        session_id,
                        account.spn,
                        account.uuid
                    );

                    Ok(Some(LdapBoundToken {
                        spn: account.spn,
                        session_id,
                        effective_session: LdapSession::UnixBind(account.uuid),
                    }))
                }
                None => Ok(None),
            }
        }
    }

    pub async fn token_auth_ldap(
        &mut self,
        lae: &LdapTokenAuthEvent,
        ct: Duration,
    ) -> Result<Option<LdapBoundToken>, OperationError> {
        match self.validate_and_parse_token_to_token(&lae.token, ct)? {
            Token::UserAuthToken(uat) => {
                let spn = uat.spn.clone();
                Ok(Some(LdapBoundToken {
                    session_id: uat.session_id,
                    spn,
                    effective_session: LdapSession::UserAuthToken(uat),
                }))
            }
            Token::ApiToken(apit, entry) => {
                let spn = entry
                    .get_ava_single_proto_string(Attribute::Spn)
                    .ok_or_else(|| OperationError::MissingAttribute(Attribute::Spn))?;

                Ok(Some(LdapBoundToken {
                    session_id: apit.token_id,
                    spn,
                    effective_session: LdapSession::ApiToken(apit),
                }))
            }
        }
    }

    pub fn commit(self) -> Result<(), OperationError> {
        Ok(())
    }
}

impl<'a> IdmServerTransaction<'a> for IdmServerProxyReadTransaction<'a> {
    type QsTransactionType = QueryServerReadTransaction<'a>;

    fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
        &mut self.qs_read
    }
}

fn gen_password_mod(
    cleartext: &str,
    crypto_policy: &CryptoPolicy,
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
    let new_cred = Credential::new_password_only(crypto_policy, cleartext)?;
    let cred_value = Value::new_credential("unix", new_cred);
    Ok(ModifyList::new_purge_and_set(
        Attribute::UnixPassword,
        cred_value,
    ))
}

fn gen_password_upgrade_mod(
    unix_cred: &Credential,
    cleartext: &str,
    crypto_policy: &CryptoPolicy,
) -> Result<Option<ModifyList<ModifyInvalid>>, OperationError> {
    if let Some(new_cred) = unix_cred.upgrade_password(crypto_policy, cleartext)? {
        let cred_value = Value::new_credential("primary", new_cred);
        Ok(Some(ModifyList::new_purge_and_set(
            Attribute::UnixPassword,
            cred_value,
        )))
    } else {
        // No action, not the same pw
        Ok(None)
    }
}

impl<'a> IdmServerProxyReadTransaction<'a> {
    pub fn jws_public_jwk(&mut self, key_id: &str) -> Result<Jwk, OperationError> {
        self.qs_read
            .get_key_providers()
            .get_key_object_handle(UUID_DOMAIN_INFO)
            // If there is no domain info, error.
            .ok_or(OperationError::NoMatchingEntries)
            .and_then(|key_object| key_object.jws_public_jwk(key_id))
            .and_then(|maybe_key: Option<Jwk>| maybe_key.ok_or(OperationError::NoMatchingEntries))
    }

    pub fn get_radiusauthtoken(
        &mut self,
        rate: &RadiusAuthTokenEvent,
        ct: Duration,
    ) -> Result<RadiusAuthToken, OperationError> {
        let account = self
            .qs_read
            .impersonate_search_ext_uuid(rate.target, &rate.ident)
            .and_then(|account_entry| {
                RadiusAccount::try_from_entry_reduced(&account_entry, &mut self.qs_read)
            })
            .map_err(|e| {
                admin_error!("Failed to start radius auth token {:?}", e);
                e
            })?;

        account.to_radiusauthtoken(ct)
    }

    pub fn get_unixusertoken(
        &mut self,
        uute: &UnixUserTokenEvent,
        ct: Duration,
    ) -> Result<UnixUserToken, OperationError> {
        let account = self
            .qs_read
            .impersonate_search_uuid(uute.target, &uute.ident)
            .and_then(|account_entry| Account::try_from_entry_ro(&account_entry, &mut self.qs_read))
            .map_err(|e| {
                admin_error!("Failed to start unix user token -> {:?}", e);
                e
            })?;

        account.to_unixusertoken(ct)
    }

    pub fn get_unixgrouptoken(
        &mut self,
        uute: &UnixGroupTokenEvent,
    ) -> Result<UnixGroupToken, OperationError> {
        let group = self
            .qs_read
            .impersonate_search_ext_uuid(uute.target, &uute.ident)
            .and_then(|e| Group::<Unix>::try_from_entry(&e))
            .map_err(|e| {
                admin_error!("Failed to start unix group token {:?}", e);
                e
            })?;
        Ok(group.to_unixgrouptoken())
    }

    pub fn get_credentialstatus(
        &mut self,
        cse: &CredentialStatusEvent,
    ) -> Result<CredentialStatus, OperationError> {
        let account = self
            .qs_read
            .impersonate_search_ext_uuid(cse.target, &cse.ident)
            .and_then(|account_entry| {
                Account::try_from_entry_reduced(&account_entry, &mut self.qs_read)
            })
            .map_err(|e| {
                admin_error!("Failed to search account {:?}", e);
                e
            })?;

        account.to_credentialstatus()
    }

    pub fn get_backup_codes(
        &mut self,
        rbce: &ReadBackupCodeEvent,
    ) -> Result<BackupCodesView, OperationError> {
        let account = self
            .qs_read
            .impersonate_search_ext_uuid(rbce.target, &rbce.ident)
            .and_then(|account_entry| {
                Account::try_from_entry_reduced(&account_entry, &mut self.qs_read)
            })
            .map_err(|e| {
                admin_error!("Failed to search account {:?}", e);
                e
            })?;

        account.to_backupcodesview()
    }
}

impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
    type QsTransactionType = QueryServerWriteTransaction<'a>;

    fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
        &mut self.qs_write
    }
}

impl<'a> IdmServerProxyWriteTransaction<'a> {
    pub(crate) fn crypto_policy(&self) -> &CryptoPolicy {
        self.crypto_policy
    }

    pub fn get_origin(&self) -> &Url {
        #[allow(clippy::unwrap_used)]
        self.webauthn.get_allowed_origins().first().unwrap()
    }

    fn check_password_quality(
        &mut self,
        cleartext: &str,
        related_inputs: &[&str],
    ) -> Result<(), OperationError> {
        // password strength and badlisting is always global, rather than per-pw-policy.
        // pw-policy as check on the account is about requirements for mfa for example.
        //

        // is the password at least 10 char?
        if cleartext.len() < PW_MIN_LENGTH as usize {
            return Err(OperationError::PasswordQuality(vec![
                PasswordFeedback::TooShort(PW_MIN_LENGTH),
            ]));
        }

        // does the password pass zxcvbn?

        let entropy = zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|e| {
            admin_error!("zxcvbn check failure (password empty?) {:?}", e);
            OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(PW_MIN_LENGTH)])
        })?;

        // Unix PW's are a single factor, so we enforce good pws
        if entropy.score() < 4 {
            // The password is too week as per:
            // https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
            let feedback: zxcvbn::feedback::Feedback = entropy
                .feedback()
                .as_ref()
                .ok_or(OperationError::InvalidState)
                .cloned()
                .inspect_err(|err| {
                    security_info!(?err, "zxcvbn returned no feedback when score < 3");
                })?;

            security_info!(?feedback, "pw quality feedback");

            // return Err(OperationError::PasswordTooWeak(feedback))
            // return Err(OperationError::PasswordTooWeak);
            return Err(OperationError::PasswordQuality(vec![
                PasswordFeedback::BadListed,
            ]));
        }

        // check a password badlist to eliminate more content
        // we check the password as "lower case" to help eliminate possibilities
        // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase)
        if self
            .qs_write
            .pw_badlist()
            .contains(&cleartext.to_lowercase())
        {
            security_info!("Password found in badlist, rejecting");
            Err(OperationError::PasswordQuality(vec![
                PasswordFeedback::BadListed,
            ]))
        } else {
            Ok(())
        }
    }

    pub(crate) fn target_to_account(&mut self, target: Uuid) -> Result<Account, OperationError> {
        // Get the account
        let account = self
            .qs_write
            .internal_search_uuid(target)
            .and_then(|account_entry| {
                Account::try_from_entry_rw(&account_entry, &mut self.qs_write)
            })
            .map_err(|e| {
                admin_error!("Failed to search account {:?}", e);
                e
            })?;
        // Ask if tis all good - this step checks pwpolicy and such

        // Deny the change if the account is anonymous!
        if account.is_anonymous() {
            admin_warn!("Unable to convert anonymous to account during write txn");
            Err(OperationError::SystemProtectedObject)
        } else {
            Ok(account)
        }
    }

    #[cfg(test)]
    pub(crate) fn set_account_password(
        &mut self,
        pce: &PasswordChangeEvent,
    ) -> Result<(), OperationError> {
        let account = self.target_to_account(pce.target)?;

        // Get the modifications we *want* to perform.
        let modlist = account
            .gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
            .map_err(|e| {
                admin_error!("Failed to generate password mod {:?}", e);
                e
            })?;
        trace!(?modlist, "processing change");

        // Check with the QS if we would be ALLOWED to do this change.

        let me = self
            .qs_write
            .impersonate_modify_gen_event(
                // Filter as executed
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
                // Filter as intended (acp)
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
                &modlist,
                &pce.ident,
            )
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })?;

        let mp = self
            .qs_write
            .modify_pre_apply(&me)
            .and_then(|opt_mp| opt_mp.ok_or(OperationError::NoMatchingEntries))
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })?;

        // If we got here, then pre-apply succeeded, and that means access control
        // passed. Now we can do the extra checks.

        // And actually really apply it now.
        self.qs_write.modify_apply(mp).map_err(|e| {
            request_error!(error = ?e);
            e
        })?;

        Ok(())
    }

    pub fn set_unix_account_password(
        &mut self,
        pce: &UnixPasswordChangeEvent,
    ) -> Result<(), OperationError> {
        // Get the account
        let account = self
            .qs_write
            .internal_search_uuid(pce.target)
            .and_then(|account_entry| {
                // Assert the account is unix and valid.
                Account::try_from_entry_rw(&account_entry, &mut self.qs_write)
            })
            .map_err(|e| {
                admin_error!("Failed to start set unix account password {:?}", e);
                e
            })?;

        // Account is not a unix account
        if account.unix_extn().is_none() {
            return Err(OperationError::MissingClass(
                ENTRYCLASS_POSIX_ACCOUNT.into(),
            ));
        }

        // Deny the change if the account is anonymous!
        if account.is_anonymous() {
            trace!("Unable to use anonymous to change UNIX account password");
            return Err(OperationError::SystemProtectedObject);
        }

        let modlist =
            gen_password_mod(pce.cleartext.as_str(), self.crypto_policy).map_err(|e| {
                admin_error!(?e, "Unable to generate password change modlist");
                e
            })?;
        trace!(?modlist, "processing change");

        // Check with the QS if we would be ALLOWED to do this change.

        let me = self
            .qs_write
            .impersonate_modify_gen_event(
                // Filter as executed
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
                // Filter as intended (acp)
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
                &modlist,
                &pce.ident,
            )
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })?;

        let mp = self
            .qs_write
            .modify_pre_apply(&me)
            .and_then(|opt_mp| opt_mp.ok_or(OperationError::NoMatchingEntries))
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })?;

        // If we got here, then pre-apply succeeded, and that means access control
        // passed. Now we can do the extra checks.

        self.check_password_quality(pce.cleartext.as_str(), account.related_inputs().as_slice())
            .map_err(|e| {
                admin_error!(?e, "Failed to checked password quality");
                e
            })?;

        // And actually really apply it now.
        self.qs_write.modify_apply(mp).map_err(|e| {
            request_error!(error = ?e);
            e
        })?;

        Ok(())
    }

    #[instrument(level = "debug", skip_all)]
    pub fn recover_account(
        &mut self,
        name: &str,
        cleartext: Option<&str>,
    ) -> Result<String, OperationError> {
        // name to uuid
        let target = self.qs_write.name_to_uuid(name).map_err(|e| {
            admin_error!(?e, "name to uuid failed");
            e
        })?;

        let cleartext = cleartext
            .map(|s| s.to_string())
            .unwrap_or_else(password_from_random);

        let ncred = Credential::new_generatedpassword_only(self.crypto_policy, &cleartext)
            .map_err(|e| {
                admin_error!("Unable to generate password mod {:?}", e);
                e
            })?;
        let vcred = Value::new_credential("primary", ncred);
        // We need to remove other credentials too.
        let modlist = ModifyList::new_list(vec![
            m_purge(Attribute::PassKeys),
            m_purge(Attribute::PrimaryCredential),
            Modify::Present(Attribute::PrimaryCredential, vcred),
        ]);

        trace!(?modlist, "processing change");

        self.qs_write
            .internal_modify(
                // Filter as executed
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target))),
                &modlist,
            )
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })?;

        Ok(cleartext)
    }

    #[instrument(level = "debug", skip_all)]
    pub fn regenerate_radius_secret(
        &mut self,
        rrse: &RegenerateRadiusSecretEvent,
    ) -> Result<String, OperationError> {
        let account = self.target_to_account(rrse.target)?;

        // Difference to the password above, this is intended to be read/copied
        // by a human wiath a keyboard in some cases.
        let cleartext = readable_password_from_random();

        // Create a modlist from the change.
        let modlist = account
            .regenerate_radius_secret_mod(cleartext.as_str())
            .map_err(|e| {
                admin_error!("Unable to generate radius secret mod {:?}", e);
                e
            })?;
        trace!(?modlist, "processing change");

        // Apply it.
        self.qs_write
            .impersonate_modify(
                // Filter as executed
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(rrse.target))),
                // Filter as intended (acp)
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(rrse.target))),
                &modlist,
                // Provide the event to impersonate
                &rrse.ident,
            )
            .map_err(|e| {
                request_error!(error = ?e);
                e
            })
            .map(|_| cleartext)
    }

    // -- delayed action processing --
    #[instrument(level = "debug", skip_all)]
    fn process_pwupgrade(&mut self, pwu: &PasswordUpgrade) -> Result<(), OperationError> {
        // get the account
        let account = self.target_to_account(pwu.target_uuid)?;

        info!(session_id = %pwu.target_uuid, "Processing password hash upgrade");

        let maybe_modlist = account
            .gen_password_upgrade_mod(pwu.existing_password.as_str(), self.crypto_policy)
            .map_err(|e| {
                admin_error!("Unable to generate password mod {:?}", e);
                e
            })?;

        if let Some(modlist) = maybe_modlist {
            self.qs_write.internal_modify(
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(pwu.target_uuid))),
                &modlist,
            )
        } else {
            // No action needed, it's probably been changed/updated already.
            Ok(())
        }
    }

    #[instrument(level = "debug", skip_all)]
    fn process_unixpwupgrade(&mut self, pwu: &UnixPasswordUpgrade) -> Result<(), OperationError> {
        info!(session_id = %pwu.target_uuid, "Processing unix password hash upgrade");

        let account = self
            .qs_write
            .internal_search_uuid(pwu.target_uuid)
            .and_then(|account_entry| {
                Account::try_from_entry_rw(&account_entry, &mut self.qs_write)
            })
            .map_err(|e| {
                admin_error!("Failed to start unix pw upgrade -> {:?}", e);
                e
            })?;

        let cred = match account.unix_extn() {
            Some(ue) => ue.ucred(),
            None => {
                return Err(OperationError::MissingClass(
                    ENTRYCLASS_POSIX_ACCOUNT.into(),
                ));
            }
        };

        // No credential no problem
        let Some(cred) = cred else {
            return Ok(());
        };

        let maybe_modlist =
            gen_password_upgrade_mod(cred, pwu.existing_password.as_str(), self.crypto_policy)?;

        match maybe_modlist {
            Some(modlist) => self.qs_write.internal_modify(
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(pwu.target_uuid))),
                &modlist,
            ),
            None => Ok(()),
        }
    }

    #[instrument(level = "debug", skip_all)]
    pub(crate) fn process_webauthncounterinc(
        &mut self,
        wci: &WebauthnCounterIncrement,
    ) -> Result<(), OperationError> {
        info!(session_id = %wci.target_uuid, "Processing webauthn counter increment");

        let mut account = self.target_to_account(wci.target_uuid)?;

        // Generate an optional mod and then attempt to apply it.
        let opt_modlist = account
            .gen_webauthn_counter_mod(&wci.auth_result)
            .map_err(|e| {
                admin_error!("Unable to generate webauthn counter mod {:?}", e);
                e
            })?;

        if let Some(modlist) = opt_modlist {
            self.qs_write.internal_modify(
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(wci.target_uuid))),
                &modlist,
            )
        } else {
            // No mod needed.
            trace!("No modification required");
            Ok(())
        }
    }

    #[instrument(level = "debug", skip_all)]
    pub(crate) fn process_backupcoderemoval(
        &mut self,
        bcr: &BackupCodeRemoval,
    ) -> Result<(), OperationError> {
        info!(session_id = %bcr.target_uuid, "Processing backup code removal");

        let account = self.target_to_account(bcr.target_uuid)?;
        // Generate an optional mod and then attempt to apply it.
        let modlist = account
            .invalidate_backup_code_mod(&bcr.code_to_remove)
            .map_err(|e| {
                admin_error!("Unable to generate backup code mod {:?}", e);
                e
            })?;

        self.qs_write.internal_modify(
            &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(bcr.target_uuid))),
            &modlist,
        )
    }

    #[instrument(level = "debug", skip_all)]
    pub(crate) fn process_authsessionrecord(
        &mut self,
        asr: &AuthSessionRecord,
    ) -> Result<(), OperationError> {
        // We have to get the entry so we can work out if we need to expire any of it's sessions.
        let state = match asr.expiry {
            Some(e) => SessionState::ExpiresAt(e),
            None => SessionState::NeverExpires,
        };

        let session = Value::Session(
            asr.session_id,
            Session {
                label: asr.label.clone(),
                state,
                // Need the other inner bits?
                // for the gracewindow.
                issued_at: asr.issued_at,
                // Who actually created this?
                issued_by: asr.issued_by.clone(),
                // Which credential was used?
                cred_id: asr.cred_id,
                // What is the access scope of this session? This is
                // for auditing purposes.
                scope: asr.scope,
                type_: asr.type_,
            },
        );

        info!(session_id = %asr.session_id, "Persisting auth session");

        // modify the account to put the session onto it.
        let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);

        self.qs_write
            .internal_modify(
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(asr.target_uuid))),
                &modlist,
            )
            .map_err(|e| {
                admin_error!("Failed to persist user auth token {:?}", e);
                e
            })
        // Done!
    }

    #[instrument(level = "debug", skip_all)]
    pub fn process_delayedaction(
        &mut self,
        da: &DelayedAction,
        _ct: Duration,
    ) -> Result<(), OperationError> {
        match da {
            DelayedAction::PwUpgrade(pwu) => self.process_pwupgrade(pwu),
            DelayedAction::UnixPwUpgrade(upwu) => self.process_unixpwupgrade(upwu),
            DelayedAction::WebauthnCounterIncrement(wci) => self.process_webauthncounterinc(wci),
            DelayedAction::BackupCodeRemoval(bcr) => self.process_backupcoderemoval(bcr),
            DelayedAction::AuthSessionRecord(asr) => self.process_authsessionrecord(asr),
        }
    }

    #[instrument(level = "debug", skip_all)]
    pub fn commit(mut self) -> Result<(), OperationError> {
        if self.qs_write.get_changed_app() {
            self.qs_write
                .get_applications_set()
                .and_then(|application_set| self.applications.reload(application_set))?;
        }
        if self.qs_write.get_changed_oauth2() {
            let domain_level = self.qs_write.get_domain_version();
            self.qs_write
                .get_oauth2rs_set()
                .and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set, domain_level))?;
            // Clear the flag to indicate we completed the reload.
            self.qs_write.clear_changed_oauth2();
        }

        // Commit everything.
        self.applications.commit();
        self.oauth2rs.commit();
        self.cred_update_sessions.commit();

        trace!("cred_update_session.commit");
        self.qs_write.commit()
    }

    #[instrument(level = "debug", skip_all)]
    pub fn generate_application_password(
        &mut self,
        ev: &GenerateApplicationPasswordEvent,
    ) -> Result<String, OperationError> {
        let account = self.target_to_account(ev.target)?;

        // This is intended to be read/copied by a human
        let cleartext = readable_password_from_random();

        // Create a modlist from the change
        let modlist = account
            .generate_application_password_mod(
                ev.application,
                ev.label.as_str(),
                cleartext.as_str(),
                self.crypto_policy,
            )
            .map_err(|e| {
                admin_error!("Unable to generate application password mod {:?}", e);
                e
            })?;
        trace!(?modlist, "processing change");
        // Apply it
        self.qs_write
            .impersonate_modify(
                // Filter as executed
                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
                // Filter as intended (acp)
                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
                &modlist,
                // Provide the event to impersonate
                &ev.ident,
            )
            .map_err(|e| {
                error!(error = ?e);
                e
            })
            .map(|_| cleartext)
    }
}

// Need tests of the sessions and the auth ...

#[cfg(test)]
mod tests {
    use std::convert::TryFrom;
    use std::time::Duration;

    use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
    use time::OffsetDateTime;
    use uuid::Uuid;

    use crate::credential::{Credential, Password};
    use crate::idm::account::DestroySessionTokenEvent;
    use crate::idm::accountpolicy::ResolvedAccountPolicy;
    use crate::idm::audit::AuditEvent;
    use crate::idm::delayed::{AuthSessionRecord, DelayedAction};
    use crate::idm::event::{AuthEvent, AuthResult};
    use crate::idm::event::{
        LdapAuthEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
        UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
    };

    use crate::idm::server::{IdmServer, IdmServerTransaction, Token};
    use crate::idm::AuthState;
    use crate::modify::{Modify, ModifyList};
    use crate::prelude::*;
    use crate::server::keys::KeyProvidersTransaction;
    use crate::value::{AuthType, SessionState};
    use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
    use kanidm_lib_crypto::CryptoPolicy;

    const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
    const TEST_PASSWORD_INC: &str = "ntaoentu nkrcgaeunhibwmwmqj;k wqjbkx ";
    const TEST_CURRENT_TIME: u64 = 6000;

    #[idm_test]
    async fn test_idm_anonymous_auth(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        // Start and test anonymous auth.
        let mut idms_auth = idms.auth().await.unwrap();
        // Send the initial auth event for initialising the session
        let anon_init = AuthEvent::anonymous_init();
        // Expect success
        let r1 = idms_auth
            .auth(
                &anon_init,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        /* Some weird lifetime things happen here ... */

        let sid = match r1 {
            Ok(ar) => {
                let AuthResult { sessionid, state } = ar;
                match state {
                    AuthState::Choose(mut conts) => {
                        // Should only be one auth mech
                        assert_eq!(conts.len(), 1);
                        // And it should be anonymous
                        let m = conts.pop().expect("Should not fail");
                        assert_eq!(m, AuthMech::Anonymous);
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-continue result!");
                        panic!();
                    }
                };
                // Now pass back the sessionid, we are good to continue.
                sessionid
            }
            Err(e) => {
                // Should not occur!
                error!("A critical error has occurred! {:?}", e);
                panic!();
            }
        };

        debug!("sessionid is ==> {:?}", sid);

        idms_auth.commit().expect("Must not fail");

        let mut idms_auth = idms.auth().await.unwrap();
        let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous);

        let r2 = idms_auth
            .auth(
                &anon_begin,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;

                match state {
                    AuthState::Continue(allowed) => {
                        // Check the uat.
                        assert_eq!(allowed.len(), 1);
                        assert_eq!(allowed.first(), Some(&AuthAllowed::Anonymous));
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-continue result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        let mut idms_auth = idms.auth().await.unwrap();
        // Now send the anonymous request, given the session id.
        let anon_step = AuthEvent::cred_step_anonymous(sid);

        // Expect success
        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;

                match state {
                    AuthState::Success(_uat, AuthIssueSession::Token) => {
                        // Check the uat.
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-success result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");
    }

    // Test sending anonymous but with no session init.
    #[idm_test]
    async fn test_idm_anonymous_auth_invalid_states(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        {
            let mut idms_auth = idms.auth().await.unwrap();
            let sid = Uuid::new_v4();
            let anon_step = AuthEvent::cred_step_anonymous(sid);

            // Expect failure
            let r2 = idms_auth
                .auth(
                    &anon_step,
                    Duration::from_secs(TEST_CURRENT_TIME),
                    Source::Internal.into(),
                )
                .await;
            debug!("r2 ==> {:?}", r2);

            match r2 {
                Ok(_) => {
                    error!("Auth state machine not correctly enforced!");
                    panic!();
                }
                Err(e) => match e {
                    OperationError::InvalidSessionState => {}
                    _ => panic!(),
                },
            };
        }
    }

    async fn init_testperson_w_password(
        idms: &IdmServer,
        pw: &str,
    ) -> Result<Uuid, OperationError> {
        let p = CryptoPolicy::minimum();
        let cred = Credential::new_password_only(&p, pw)?;
        let cred_id = cred.uuid;
        let v_cred = Value::new_credential("primary", cred);
        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        idms_write
            .qs_write
            .internal_create(vec![E_TESTPERSON_1.clone()])
            .expect("Failed to create test person");

        // now modify and provide a primary credential.
        let me_inv_m = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
            ModifyList::new_list(vec![Modify::Present(Attribute::PrimaryCredential, v_cred)]),
        );
        // go!
        assert!(idms_write.qs_write.modify(&me_inv_m).is_ok());

        idms_write.commit().map(|()| cred_id)
    }

    async fn init_authsession_sid(idms: &IdmServer, ct: Duration, name: &str) -> Uuid {
        let mut idms_auth = idms.auth().await.unwrap();
        let admin_init = AuthEvent::named_init(name);

        let r1 = idms_auth
            .auth(&admin_init, ct, Source::Internal.into())
            .await;
        let ar = r1.unwrap();
        let AuthResult { sessionid, state } = ar;

        assert!(matches!(state, AuthState::Choose(_)));

        // 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, Source::Internal.into())
            .await;
        let ar = r2.unwrap();
        let AuthResult { sessionid, state } = ar;

        match state {
            AuthState::Continue(_) => {}
            s => {
                error!(?s, "Sessions was not initialised");
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        sessionid
    }

    async fn check_testperson_password(idms: &IdmServer, pw: &str, ct: Duration) -> JwsCompact {
        let sid = init_authsession_sid(idms, ct, "testperson1").await;

        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid, pw);

        // Expect success
        let r2 = idms_auth
            .auth(&anon_step, ct, Source::Internal.into())
            .await;
        debug!("r2 ==> {:?}", r2);

        let token = match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;

                match state {
                    AuthState::Success(token, AuthIssueSession::Token) => {
                        // Check the uat.
                        token
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-success result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        *token
    }

    #[idm_test]
    async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
        let ct = duration_from_epoch_now();
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        check_testperson_password(idms, TEST_PASSWORD, ct).await;

        // Clear our the session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        idms_delayed.check_is_empty_or_panic();
    }

    #[idm_test]
    async fn test_idm_simple_password_spn_auth(
        idms: &IdmServer,
        idms_delayed: &mut IdmServerDelayed,
    ) {
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");

        let sid = init_authsession_sid(
            idms,
            Duration::from_secs(TEST_CURRENT_TIME),
            "testperson1@example.com",
        )
        .await;

        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD);

        // Expect success
        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Success(_uat, AuthIssueSession::Token) => {
                        // Check the uat.
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-success result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        // Clear our the session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        idms_delayed.check_is_empty_or_panic();

        idms_auth.commit().expect("Must not fail");
    }

    #[idm_test(audit = 1)]
    async fn test_idm_simple_password_invalid(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
        idms_audit: &mut IdmServerAudit,
    ) {
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        let sid =
            init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);

        // Expect success
        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Denied(_reason) => {
                        // Check the uat.
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-denied result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        // There should be a queued audit event
        match idms_audit.audit_rx().try_recv() {
            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
            _ => panic!("Oh no"),
        }

        idms_auth.commit().expect("Must not fail");
    }

    #[idm_test]
    async fn test_idm_simple_password_reset(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let pce = PasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);

        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        assert!(idms_prox_write.set_account_password(&pce).is_ok());
        assert!(idms_prox_write.set_account_password(&pce).is_ok());
        assert!(idms_prox_write.commit().is_ok());
    }

    #[idm_test]
    async fn test_idm_anonymous_set_password_denied(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let pce = PasswordChangeEvent::new_internal(UUID_ANONYMOUS, TEST_PASSWORD);

        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        assert!(idms_prox_write.set_account_password(&pce).is_err());
        assert!(idms_prox_write.commit().is_ok());
    }

    #[idm_test]
    async fn test_idm_regenerate_radius_secret(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        idms_prox_write
            .qs_write
            .internal_create(vec![E_TESTPERSON_1.clone()])
            .expect("unable to create test person");

        let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);

        // Generates a new credential when none exists
        let r1 = idms_prox_write
            .regenerate_radius_secret(&rrse)
            .expect("Failed to reset radius credential 1");
        // Regenerates and overwrites the radius credential
        let r2 = idms_prox_write
            .regenerate_radius_secret(&rrse)
            .expect("Failed to reset radius credential 2");
        assert!(r1 != r2);
    }

    #[idm_test]
    async fn test_idm_radiusauthtoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        idms_prox_write
            .qs_write
            .internal_create(vec![E_TESTPERSON_1.clone()])
            .expect("unable to create test person");

        let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
        let r1 = idms_prox_write
            .regenerate_radius_secret(&rrse)
            .expect("Failed to reset radius credential 1");
        idms_prox_write.commit().expect("failed to commit");

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let person_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("Can't access admin entry.");

        let rate = RadiusAuthTokenEvent::new_impersonate(person_entry, UUID_TESTPERSON_1);
        let tok_r = idms_prox_read
            .get_radiusauthtoken(&rate, duration_from_epoch_now())
            .expect("Failed to generate radius auth token");

        // view the token?
        assert_eq!(r1, tok_r.secret);
    }

    #[idm_test]
    async fn test_idm_unixusertoken(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        // Modify admin to have posixaccount
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
        // Add a posix group that has the admin as a member.
        let e: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::PosixGroup.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (
                Attribute::Uuid,
                Value::Uuid(uuid::uuid!("01609135-a1c4-43d5-966b-a28227644445"))
            ),
            (Attribute::Description, Value::new_utf8s("testgroup")),
            (
                Attribute::Member,
                Value::Refer(uuid::uuid!("00000000-0000-0000-0000-000000000000"))
            )
        );

        let ce = CreateEvent::new_internal(vec![e]);

        assert!(idms_prox_write.qs_write.create(&ce).is_ok());

        idms_prox_write.commit().expect("failed to commit");

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        // Get the account that will be doing the actual reads.
        let admin_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_ADMIN)
            .expect("Can't access admin entry.");

        let ugte = UnixGroupTokenEvent::new_impersonate(
            admin_entry.clone(),
            uuid!("01609135-a1c4-43d5-966b-a28227644445"),
        );
        let tok_g = idms_prox_read
            .get_unixgrouptoken(&ugte)
            .expect("Failed to generate unix group token");

        assert_eq!(tok_g.name, "testgroup");
        assert_eq!(tok_g.spn, "testgroup@example.com");

        let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN);
        let tok_r = idms_prox_read
            .get_unixusertoken(&uute, duration_from_epoch_now())
            .expect("Failed to generate unix user token");

        assert_eq!(tok_r.name, "admin");
        assert_eq!(tok_r.spn, "admin@example.com");
        assert_eq!(tok_r.groups.len(), 2);
        assert_eq!(tok_r.groups[0].name, "admin");
        assert_eq!(tok_r.groups[1].name, "testgroup");
        assert!(tok_r.valid);

        // Show we can get the admin as a unix group token too
        let ugte = UnixGroupTokenEvent::new_impersonate(
            admin_entry,
            uuid!("00000000-0000-0000-0000-000000000000"),
        );
        let tok_g = idms_prox_read
            .get_unixgrouptoken(&ugte)
            .expect("Failed to generate unix group token");

        assert_eq!(tok_g.name, "admin");
        assert_eq!(tok_g.spn, "admin@example.com");
    }

    #[idm_test]
    async fn test_idm_simple_unix_password_reset(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        // make the admin a valid posix account
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());

        let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);

        assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        let mut idms_auth = idms.auth().await.unwrap();
        // Check auth verification of the password

        let uuae_good = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
        let a1 = idms_auth
            .auth_unix(&uuae_good, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a1 {
            Ok(Some(_tok)) => {}
            _ => panic!("Oh no"),
        };
        // Check bad password
        let uuae_bad = UnixUserAuthEvent::new_internal(UUID_ADMIN, TEST_PASSWORD_INC);
        let a2 = idms_auth
            .auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a2 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };
        assert!(idms_auth.commit().is_ok());

        // Check deleting the password
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let me_purge_up = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
            ModifyList::new_list(vec![Modify::Purged(Attribute::UnixPassword)]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_purge_up).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        // And auth should now fail due to the lack of PW material (note that
        // softlocking WON'T kick in because the cred_uuid is gone!)
        let mut idms_auth = idms.auth().await.unwrap();
        let a3 = idms_auth
            .auth_unix(&uuae_good, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a3 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };
        assert!(idms_auth.commit().is_ok());
    }

    #[idm_test]
    async fn test_idm_simple_password_upgrade(
        idms: &IdmServer,
        idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = duration_from_epoch_now();
        // Assert the delayed action queue is empty
        idms_delayed.check_is_empty_or_panic();
        // Setup the admin w_ an imported password.
        {
            let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
            // now modify and provide a primary credential.

            idms_prox_write
                .qs_write
                .internal_create(vec![E_TESTPERSON_1.clone()])
                .expect("Failed to create test person");

            let me_inv_m =
                ModifyEvent::new_internal_invalid(
                        filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
                        ModifyList::new_list(vec![Modify::Present(
                            Attribute::PasswordImport,
                            Value::from("{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM")
                        )]),
                    );
            // go!
            assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
            assert!(idms_prox_write.commit().is_ok());
        }
        // Still empty
        idms_delayed.check_is_empty_or_panic();

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let person_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("Can't access admin entry.");
        let cred_before = person_entry
            .get_ava_single_credential(Attribute::PrimaryCredential)
            .expect("No credential present")
            .clone();
        drop(idms_prox_read);

        // Do an auth, this will trigger the action to send.
        check_testperson_password(idms, "password", ct).await;

        // ⚠️  We have to be careful here. Between these two actions, it's possible
        // that on the pw upgrade that the credential uuid changes. This immediately
        // causes the session to be invalidated.

        // We need to check the credential id does not change between these steps to
        // prevent this!

        // process it.
        let da = idms_delayed.try_recv().expect("invalid");
        // The first task is the pw upgrade
        assert!(matches!(da, DelayedAction::PwUpgrade(_)));
        let r = idms.delayed_action(duration_from_epoch_now(), da).await;
        // The second is the auth session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        assert_eq!(Ok(true), r);

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let person_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("Can't access admin entry.");
        let cred_after = person_entry
            .get_ava_single_credential(Attribute::PrimaryCredential)
            .expect("No credential present")
            .clone();
        drop(idms_prox_read);

        assert_eq!(cred_before.uuid, cred_after.uuid);

        // Check the admin pw still matches
        check_testperson_password(idms, "password", ct).await;
        // Clear the next auth session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));

        // No delayed action was queued.
        idms_delayed.check_is_empty_or_panic();
    }

    #[idm_test]
    async fn test_idm_unix_password_upgrade(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
        // Assert the delayed action queue is empty
        idms_delayed.check_is_empty_or_panic();
        // Setup the admin with an imported unix pw.
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        let im_pw = "{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
        let pw = Password::try_from(im_pw).expect("failed to parse");
        let cred = Credential::new_from_password(pw);
        let v_cred = Value::new_credential("unix", cred);

        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
                Modify::Present(Attribute::UnixPassword, v_cred),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
        assert!(idms_prox_write.commit().is_ok());
        idms_delayed.check_is_empty_or_panic();
        // Get the auth ready.
        let uuae = UnixUserAuthEvent::new_internal(UUID_ADMIN, "password");
        let mut idms_auth = idms.auth().await.unwrap();
        let a1 = idms_auth
            .auth_unix(&uuae, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a1 {
            Ok(Some(_tok)) => {}
            _ => panic!("Oh no"),
        };
        idms_auth.commit().expect("Must not fail");
        // The upgrade was queued
        // Process it.
        let da = idms_delayed.try_recv().expect("invalid");
        let _r = idms.delayed_action(duration_from_epoch_now(), da).await;
        // Go again
        let mut idms_auth = idms.auth().await.unwrap();
        let a2 = idms_auth
            .auth_unix(&uuae, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a2 {
            Ok(Some(_tok)) => {}
            _ => panic!("Oh no"),
        };
        idms_auth.commit().expect("Must not fail");
        // No delayed action was queued.
        idms_delayed.check_is_empty_or_panic();
    }

    // For testing the timeouts
    // We need times on this scale
    //    not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
    const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
    const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
    const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
    const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;

    async fn set_testperson_valid_time(idms: &IdmServer) {
        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));

        // now modify and provide a primary credential.
        let me_inv_m = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::AccountExpire, v_expire),
                Modify::Present(Attribute::AccountValidFrom, v_valid_from),
            ]),
        );
        // go!
        assert!(idms_write.qs_write.modify(&me_inv_m).is_ok());

        idms_write.commit().expect("Must not fail");
    }

    #[idm_test]
    async fn test_idm_account_valid_from_expire(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        // Any account that is not yet valrid / expired can't auth.

        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        // Set the valid bounds high/low
        // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
        set_testperson_valid_time(idms).await;

        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);

        let mut idms_auth = idms.auth().await.unwrap();
        let admin_init = AuthEvent::named_init("admin");
        let r1 = idms_auth
            .auth(&admin_init, time_low, Source::Internal.into())
            .await;

        let ar = r1.unwrap();
        let AuthResult {
            sessionid: _,
            state,
        } = ar;

        match state {
            AuthState::Denied(_) => {}
            _ => {
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        // And here!
        let mut idms_auth = idms.auth().await.unwrap();
        let admin_init = AuthEvent::named_init("admin");
        let r1 = idms_auth
            .auth(&admin_init, time_high, Source::Internal.into())
            .await;

        let ar = r1.unwrap();
        let AuthResult {
            sessionid: _,
            state,
        } = ar;

        match state {
            AuthState::Denied(_) => {}
            _ => {
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");
    }

    #[idm_test]
    async fn test_idm_unix_valid_from_expire(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        // Any account that is expired can't unix auth.
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        set_testperson_valid_time(idms).await;

        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);

        // make the admin a valid posix account
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());

        let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);

        assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        // Now check auth when the time is too high or too low.
        let mut idms_auth = idms.auth().await.unwrap();
        let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);

        let a1 = idms_auth.auth_unix(&uuae_good, time_low).await;
        // Should this actually send an error with the details? Or just silently act as
        // badpw?
        match a1 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };

        let a2 = idms_auth.auth_unix(&uuae_good, time_high).await;
        match a2 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };

        idms_auth.commit().expect("Must not fail");
        // Also check the generated unix tokens are invalid.
        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let uute = UnixUserTokenEvent::new_internal(UUID_TESTPERSON_1);

        let tok_r = idms_prox_read
            .get_unixusertoken(&uute, time_low)
            .expect("Failed to generate unix user token");

        assert_eq!(tok_r.name, "testperson1");
        assert!(!tok_r.valid);

        let tok_r = idms_prox_read
            .get_unixusertoken(&uute, time_high)
            .expect("Failed to generate unix user token");

        assert_eq!(tok_r.name, "testperson1");
        assert!(!tok_r.valid);
    }

    #[idm_test]
    async fn test_idm_radius_valid_from_expire(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        // Any account not valid/expiry should not return
        // a radius packet.
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        set_testperson_valid_time(idms).await;

        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);

        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_TESTPERSON_1);
        let _r1 = idms_prox_write
            .regenerate_radius_secret(&rrse)
            .expect("Failed to reset radius credential 1");
        idms_prox_write.commit().expect("failed to commit");

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let admin_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_ADMIN)
            .expect("Can't access admin entry.");

        let rate = RadiusAuthTokenEvent::new_impersonate(admin_entry, UUID_ADMIN);
        let tok_r = idms_prox_read.get_radiusauthtoken(&rate, time_low);

        if tok_r.is_err() {
            // Ok?
        } else {
            debug_assert!(false);
        }

        let tok_r = idms_prox_read.get_radiusauthtoken(&rate, time_high);

        if tok_r.is_err() {
            // Ok?
        } else {
            debug_assert!(false);
        }
    }

    #[idm_test(audit = 1)]
    async fn test_idm_account_softlocking(
        idms: &IdmServer,
        idms_delayed: &mut IdmServerDelayed,
        idms_audit: &mut IdmServerAudit,
    ) {
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");

        // Auth invalid, no softlock present.
        let sid =
            init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC);

        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Denied(reason) => {
                        assert!(reason != "Account is temporarily locked");
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-denied result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                panic!();
            }
        };

        // There should be a queued audit event
        match idms_audit.audit_rx().try_recv() {
            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
            _ => panic!("Oh no"),
        }

        idms_auth.commit().expect("Must not fail");

        // Auth init, softlock present, count == 1, same time (so before unlock_at)
        // aka Auth valid immediate, (ct < exp), autofail
        // aka Auth invalid immediate, (ct < exp), autofail
        let mut idms_auth = idms.auth().await.unwrap();
        let admin_init = AuthEvent::named_init("testperson1");

        let r1 = idms_auth
            .auth(
                &admin_init,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        let ar = r1.unwrap();
        let AuthResult { sessionid, state } = ar;
        assert!(matches!(state, AuthState::Choose(_)));

        // Soft locks only apply once a mechanism is chosen
        let admin_begin = AuthEvent::begin_mech(sessionid, AuthMech::Password);

        let r2 = idms_auth
            .auth(
                &admin_begin,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        let ar = r2.unwrap();
        let AuthResult {
            sessionid: _,
            state,
        } = ar;

        match state {
            AuthState::Denied(reason) => {
                assert_eq!(reason, "Account is temporarily locked");
            }
            _ => {
                error!("Sessions was not denied (softlock)");
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        // Auth invalid once softlock pass (count == 2, exp_at grows)
        // Tested in the softlock state machine.

        // Auth valid once softlock pass, valid. Count remains.
        let sid = init_authsession_sid(
            idms,
            Duration::from_secs(TEST_CURRENT_TIME + 2),
            "testperson1",
        )
        .await;

        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD);

        // Expect success
        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME + 2),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Success(_uat, AuthIssueSession::Token) => {
                        // Check the uat.
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-success result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        // Clear the auth session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        idms_delayed.check_is_empty_or_panic();

        // Auth valid after reset at, count == 0.
        // Tested in the softlock state machine.

        // Auth invalid, softlock present, count == 1
        // Auth invalid after reset at, count == 0 and then to count == 1
        // Tested in the softlock state machine.
    }

    #[idm_test(audit = 1)]
    async fn test_idm_account_softlocking_interleaved(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
        idms_audit: &mut IdmServerAudit,
    ) {
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");

        // Start an *early* auth session.
        let sid_early =
            init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;

        // Start a second auth session
        let sid_later =
            init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
        // Get the detail wrong in sid_later.
        let mut idms_auth = idms.auth().await.unwrap();
        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),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Denied(reason) => {
                        assert!(reason != "Account is temporarily locked");
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-denied result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                panic!();
            }
        };

        match idms_audit.audit_rx().try_recv() {
            Ok(AuditEvent::AuthenticationDenied { .. }) => {}
            _ => panic!("Oh no"),
        }

        idms_auth.commit().expect("Must not fail");

        // Now check that sid_early is denied due to softlock.
        let mut idms_auth = idms.auth().await.unwrap();
        let anon_step = AuthEvent::cred_step_password(sid_early, TEST_PASSWORD);

        // Expect success
        let r2 = idms_auth
            .auth(
                &anon_step,
                Duration::from_secs(TEST_CURRENT_TIME),
                Source::Internal.into(),
            )
            .await;
        debug!("r2 ==> {:?}", r2);
        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;
                match state {
                    AuthState::Denied(reason) => {
                        assert_eq!(reason, "Account is temporarily locked");
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-denied result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                panic!();
            }
        };
        idms_auth.commit().expect("Must not fail");
    }

    #[idm_test]
    async fn test_idm_account_unix_softlocking(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        // make the admin a valid posix account
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_TESTPERSON_1))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());

        let pce = UnixPasswordChangeEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
        assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        let mut idms_auth = idms.auth().await.unwrap();
        let uuae_good = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD);
        let uuae_bad = UnixUserAuthEvent::new_internal(UUID_TESTPERSON_1, TEST_PASSWORD_INC);

        let a2 = idms_auth
            .auth_unix(&uuae_bad, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a2 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };

        // Now if we immediately auth again, should fail at same time due to SL
        let a1 = idms_auth
            .auth_unix(&uuae_good, Duration::from_secs(TEST_CURRENT_TIME))
            .await;
        match a1 {
            Ok(None) => {}
            _ => panic!("Oh no"),
        };

        // And then later, works because of SL lifting.
        let a1 = idms_auth
            .auth_unix(&uuae_good, Duration::from_secs(TEST_CURRENT_TIME + 2))
            .await;
        match a1 {
            Ok(Some(_tok)) => {}
            _ => panic!("Oh no"),
        };

        assert!(idms_auth.commit().is_ok());
    }

    #[idm_test]
    async fn test_idm_jwt_uat_expiry(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let expiry = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
        // Do an authenticate
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        let token = check_testperson_password(idms, TEST_PASSWORD, ct).await;

        // Clear out the queued session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        // Persist it.
        let r = idms.delayed_action(ct, da).await;
        assert_eq!(Ok(true), r);
        idms_delayed.check_is_empty_or_panic();

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        // Check it's valid - This is within the time window so will pass.
        idms_prox_read
            .validate_client_auth_info_to_ident(token.clone().into(), ct)
            .expect("Failed to validate");

        // In X time it should be INVALID
        match idms_prox_read.validate_client_auth_info_to_ident(token.into(), expiry) {
            Err(OperationError::SessionExpired) => {}
            _ => panic!("Oh no"),
        }
    }

    #[idm_test]
    async fn test_idm_expired_auth_session_cleanup(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let expiry_a = ct + Duration::from_secs((DEFAULT_AUTH_SESSION_EXPIRY + 1).into());
        let expiry_b = ct + Duration::from_secs(((DEFAULT_AUTH_SESSION_EXPIRY + 1) * 2).into());

        let session_a = Uuid::new_v4();
        let session_b = Uuid::new_v4();

        // We need to put the credential on the admin.
        let cred_id = init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");

        // Assert no sessions present
        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let admin = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("failed");
        let sessions = admin.get_ava_as_session_map(Attribute::UserAuthTokenSession);
        assert!(sessions.is_none());
        drop(idms_prox_read);

        let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
            target_uuid: UUID_TESTPERSON_1,
            session_id: session_a,
            cred_id,
            label: "Test Session A".to_string(),
            expiry: Some(OffsetDateTime::UNIX_EPOCH + expiry_a),
            issued_at: OffsetDateTime::UNIX_EPOCH + ct,
            issued_by: IdentityId::User(UUID_ADMIN),
            scope: SessionScope::ReadOnly,
            type_: AuthType::Passkey,
        });
        // Persist it.
        let r = idms.delayed_action(ct, da).await;
        assert_eq!(Ok(true), r);

        // Check it was written, and check
        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let admin = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("failed");
        let sessions = admin
            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
            .expect("Sessions must be present!");
        assert_eq!(sessions.len(), 1);
        let session_data_a = sessions.get(&session_a).expect("Session A is missing!");
        assert!(matches!(session_data_a.state, SessionState::ExpiresAt(_)));

        drop(idms_prox_read);

        // When we re-auth, this is what triggers the session revoke via the delayed action.

        let da = DelayedAction::AuthSessionRecord(AuthSessionRecord {
            target_uuid: UUID_TESTPERSON_1,
            session_id: session_b,
            cred_id,
            label: "Test Session B".to_string(),
            expiry: Some(OffsetDateTime::UNIX_EPOCH + expiry_b),
            issued_at: OffsetDateTime::UNIX_EPOCH + ct,
            issued_by: IdentityId::User(UUID_ADMIN),
            scope: SessionScope::ReadOnly,
            type_: AuthType::Passkey,
        });
        // Persist it.
        let r = idms.delayed_action(expiry_a, da).await;
        assert_eq!(Ok(true), r);

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        let admin = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_TESTPERSON_1)
            .expect("failed");
        let sessions = admin
            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
            .expect("Sessions must be present!");
        trace!(?sessions);
        assert_eq!(sessions.len(), 2);

        let session_data_a = sessions.get(&session_a).expect("Session A is missing!");
        assert!(matches!(session_data_a.state, SessionState::RevokedAt(_)));

        let session_data_b = sessions.get(&session_b).expect("Session B is missing!");
        assert!(matches!(session_data_b.state, SessionState::ExpiresAt(_)));
        // Now show that sessions trim!
    }

    #[idm_test]
    async fn test_idm_account_session_validation(
        idms: &IdmServer,
        idms_delayed: &mut IdmServerDelayed,
    ) {
        use kanidm_proto::internal::UserAuthToken;

        let ct = duration_from_epoch_now();

        let post_grace = ct + AUTH_TOKEN_GRACE_WINDOW + Duration::from_secs(1);
        let expiry = ct + Duration::from_secs(DEFAULT_AUTH_SESSION_EXPIRY as u64 + 1);

        // Assert that our grace time is less than expiry, so we know the failure is due to
        // this.
        assert!(post_grace < expiry);

        // Do an authenticate
        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        let uat_unverified = check_testperson_password(idms, TEST_PASSWORD, ct).await;

        // Process the session info.
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        let r = idms.delayed_action(ct, da).await;
        assert_eq!(Ok(true), r);

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        let token_kid = uat_unverified.kid().expect("no key id present");

        let uat_jwk = idms_prox_read
            .qs_read
            .get_key_providers()
            .get_key_object(UUID_DOMAIN_INFO)
            .and_then(|object| {
                object
                    .jws_public_jwk(token_kid)
                    .expect("Unable to access uat jwk")
            })
            .expect("No jwk by this kid");

        let jws_validator = JwsEs256Verifier::try_from(&uat_jwk).unwrap();

        let uat_inner: UserAuthToken = jws_validator
            .verify(&uat_unverified)
            .unwrap()
            .from_json()
            .unwrap();

        // Check it's valid.
        idms_prox_read
            .validate_client_auth_info_to_ident(uat_unverified.clone().into(), ct)
            .expect("Failed to validate");

        // If the auth session record wasn't processed, this will fail.
        idms_prox_read
            .validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
            .expect("Failed to validate");

        drop(idms_prox_read);

        // Mark the session as invalid now.
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
        let dte = DestroySessionTokenEvent::new_internal(uat_inner.uuid, uat_inner.session_id);
        assert!(idms_prox_write.account_destroy_session_token(&dte).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        // Now check again with the session destroyed.
        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        // Now, within gracewindow, it's NOT valid because the session entry exists and is in
        // the revoked state!
        match idms_prox_read
            .validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
        {
            Err(OperationError::SessionExpired) => {}
            _ => panic!("Oh no"),
        }
        drop(idms_prox_read);

        // Force trim the session out so that we can check the grate handling.
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
        let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(uat_inner.uuid)));
        let mut work_set = idms_prox_write
            .qs_write
            .internal_search_writeable(&filt)
            .expect("Failed to perform internal search writeable");
        for (_, entry) in work_set.iter_mut() {
            let _ = entry.force_trim_ava(Attribute::UserAuthTokenSession);
        }
        assert!(idms_prox_write
            .qs_write
            .internal_apply_writable(work_set)
            .is_ok());

        assert!(idms_prox_write.commit().is_ok());

        let mut idms_prox_read = idms.proxy_read().await.unwrap();
        idms_prox_read
            .validate_client_auth_info_to_ident(uat_unverified.clone().into(), ct)
            .expect("Failed to validate");

        // post grace, it's not valid.
        match idms_prox_read
            .validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
        {
            Err(OperationError::SessionExpired) => {}
            _ => panic!("Oh no"),
        }
    }

    #[idm_test]
    async fn test_idm_account_session_expiry(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);

        //we first set the expiry to a custom value
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

        let new_authsession_expiry = 1000;

        let modlist = ModifyList::new_purge_and_set(
            Attribute::AuthSessionExpiry,
            Value::Uint32(new_authsession_expiry),
        );
        idms_prox_write
            .qs_write
            .internal_modify_uuid(UUID_IDM_ALL_ACCOUNTS, &modlist)
            .expect("Unable to change default session exp");

        assert!(idms_prox_write.commit().is_ok());

        // Start anonymous auth.
        let mut idms_auth = idms.auth().await.unwrap();
        // Send the initial auth event for initialising the session
        let anon_init = AuthEvent::anonymous_init();
        // Expect success
        let r1 = idms_auth
            .auth(&anon_init, ct, Source::Internal.into())
            .await;
        /* Some weird lifetime things happen here ... */

        let sid = match r1 {
            Ok(ar) => {
                let AuthResult { sessionid, state } = ar;
                match state {
                    AuthState::Choose(mut conts) => {
                        // Should only be one auth mech
                        assert_eq!(conts.len(), 1);
                        // And it should be anonymous
                        let m = conts.pop().expect("Should not fail");
                        assert_eq!(m, AuthMech::Anonymous);
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-continue result!");
                        panic!();
                    }
                };
                // Now pass back the sessionid, we are good to continue.
                sessionid
            }
            Err(e) => {
                // Should not occur!
                error!("A critical error has occurred! {:?}", e);
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        let mut idms_auth = idms.auth().await.unwrap();
        let anon_begin = AuthEvent::begin_mech(sid, AuthMech::Anonymous);

        let r2 = idms_auth
            .auth(&anon_begin, ct, Source::Internal.into())
            .await;

        match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;

                match state {
                    AuthState::Continue(allowed) => {
                        // Check the uat.
                        assert_eq!(allowed.len(), 1);
                        assert_eq!(allowed.first(), Some(&AuthAllowed::Anonymous));
                    }
                    _ => {
                        error!("A critical error has occurred! We have a non-continue result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!();
            }
        };

        idms_auth.commit().expect("Must not fail");

        let mut idms_auth = idms.auth().await.unwrap();
        // Now send the anonymous request, given the session id.
        let anon_step = AuthEvent::cred_step_anonymous(sid);

        // Expect success
        let r2 = idms_auth
            .auth(&anon_step, ct, Source::Internal.into())
            .await;

        let token = match r2 {
            Ok(ar) => {
                let AuthResult {
                    sessionid: _,
                    state,
                } = ar;

                match state {
                    AuthState::Success(uat, AuthIssueSession::Token) => uat,
                    _ => {
                        error!("A critical error has occurred! We have a non-success result!");
                        panic!();
                    }
                }
            }
            Err(e) => {
                error!("A critical error has occurred! {:?}", e);
                // Should not occur!
                panic!("A critical error has occurred! {:?}", e);
            }
        };

        idms_auth.commit().expect("Must not fail");

        // Token_str to uat
        // we have to do it this way because anonymous doesn't have an ideantity for which we cam get the expiry value
        let Token::UserAuthToken(uat) = idms
            .proxy_read()
            .await
            .unwrap()
            .validate_and_parse_token_to_token(&token, ct)
            .expect("Must not fail")
        else {
            panic!("Unexpected auth token type for anonymous auth");
        };

        debug!(?uat);

        assert!(
            matches!(uat.expiry, Some(exp) if exp == OffsetDateTime::UNIX_EPOCH + ct + Duration::from_secs(new_authsession_expiry as u64))
        );
    }

    #[idm_test]
    async fn test_idm_uat_claim_insertion(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

        // get an account.
        let account = idms_prox_write
            .target_to_account(UUID_ADMIN)
            .expect("account must exist");

        // Create some fake UATs, then process them and see what claims fall out 🥳
        let session_id = uuid::Uuid::new_v4();

        // For the different auth types, check that we get the correct claims:

        // == anonymous
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_anonymous"));
        // Does NOT have this
        assert!(!ident.has_claim("authlevel_strong"));
        assert!(!ident.has_claim("authclass_single"));
        assert!(!ident.has_claim("authclass_mfa"));

        // == unixpassword
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_unixpassword"));
        assert!(!ident.has_claim("authclass_single"));
        // Does NOT have this
        assert!(!ident.has_claim("authlevel_strong"));
        assert!(!ident.has_claim("authclass_mfa"));

        // == password
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_password"));
        assert!(!ident.has_claim("authclass_single"));
        // Does NOT have this
        assert!(!ident.has_claim("authlevel_strong"));
        assert!(!ident.has_claim("authclass_mfa"));

        // == generatedpassword
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_generatedpassword"));
        assert!(!ident.has_claim("authclass_single"));
        assert!(!ident.has_claim("authlevel_strong"));
        // Does NOT have this
        assert!(!ident.has_claim("authclass_mfa"));

        // == webauthn
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_webauthn"));
        assert!(!ident.has_claim("authclass_single"));
        assert!(!ident.has_claim("authlevel_strong"));
        // Does NOT have this
        assert!(!ident.has_claim("authclass_mfa"));

        // == passwordmfa
        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");
        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert!(!ident.has_claim("authtype_passwordmfa"));
        assert!(!ident.has_claim("authlevel_strong"));
        assert!(!ident.has_claim("authclass_mfa"));
        // Does NOT have this
        assert!(!ident.has_claim("authclass_single"));
    }

    #[idm_test]
    async fn test_idm_uat_limits_account_policy(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

        idms_prox_write
            .qs_write
            .internal_create(vec![E_TESTPERSON_1.clone()])
            .expect("Failed to create test person");

        // get an account.
        let account = idms_prox_write
            .target_to_account(UUID_TESTPERSON_1)
            .expect("account must exist");

        // Create a fake UATs
        let session_id = uuid::Uuid::new_v4();

        let uat = account
            .to_userauthtoken(
                session_id,
                SessionScope::ReadWrite,
                ct,
                &ResolvedAccountPolicy::test_policy(),
            )
            .expect("Unable to create uat");

        let ident = idms_prox_write
            .process_uat_to_identity(&uat, ct, Source::Internal)
            .expect("Unable to process uat");

        assert_eq!(
            ident.limits().search_max_results,
            DEFAULT_LIMIT_SEARCH_MAX_RESULTS as usize
        );
        assert_eq!(
            ident.limits().search_max_filter_test,
            DEFAULT_LIMIT_SEARCH_MAX_FILTER_TEST as usize
        );
    }

    #[idm_test]
    async fn test_idm_jwt_uat_token_key_reload(
        idms: &IdmServer,
        idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = duration_from_epoch_now();

        init_testperson_w_password(idms, TEST_PASSWORD)
            .await
            .expect("Failed to setup admin account");
        let token = check_testperson_password(idms, TEST_PASSWORD, ct).await;

        // Clear the session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        idms_delayed.check_is_empty_or_panic();

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        // Check it's valid.
        idms_prox_read
            .validate_client_auth_info_to_ident(token.clone().into(), ct)
            .expect("Failed to validate");

        drop(idms_prox_read);

        // We need to get the token key id and revoke it.
        let revoke_kid = token.kid().expect("token does not contain a key id");

        // Now revoke the token_key
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
        let me_reset_tokens = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
            ModifyList::new_append(
                Attribute::KeyActionRevoke,
                Value::HexString(revoke_kid.to_string()),
            ),
        );
        assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        let new_token = check_testperson_password(idms, TEST_PASSWORD, ct).await;

        // Clear the session record
        let da = idms_delayed.try_recv().expect("invalid");
        assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
        idms_delayed.check_is_empty_or_panic();

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        // Check the old token is invalid, due to reload.
        assert!(idms_prox_read
            .validate_client_auth_info_to_ident(token.into(), ct)
            .is_err());

        // A new token will work due to the matching key.
        idms_prox_read
            .validate_client_auth_info_to_ident(new_token.into(), ct)
            .expect("Failed to validate");
    }

    #[idm_test]
    async fn test_idm_service_account_to_person(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

        let ident = Identity::from_internal();
        let target_uuid = Uuid::new_v4();

        // Create a service account
        let e = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Account.to_value()),
            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
            (Attribute::Name, Value::new_iname("testaccount")),
            (Attribute::Uuid, Value::Uuid(target_uuid)),
            (Attribute::Description, Value::new_utf8s("testaccount")),
            (Attribute::DisplayName, Value::new_utf8s("Test Account"))
        );

        let ce = CreateEvent::new_internal(vec![e]);
        let cr = idms_prox_write.qs_write.create(&ce);
        assert!(cr.is_ok());

        // Do the migrate.
        assert!(idms_prox_write
            .service_account_into_person(&ident, target_uuid)
            .is_ok());

        // Any checks?
    }

    async fn idm_fallback_auth_fixture(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
        has_posix_password: bool,
        allow_primary_cred_fallback: Option<bool>,
        expected: Option<()>,
    ) {
        let ct = Duration::from_secs(TEST_CURRENT_TIME);
        let target_uuid = Uuid::new_v4();
        let p = CryptoPolicy::minimum();

        {
            let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

            if let Some(allow_primary_cred_fallback) = allow_primary_cred_fallback {
                idms_prox_write
                    .qs_write
                    .internal_modify_uuid(
                        UUID_IDM_ALL_ACCOUNTS,
                        &ModifyList::new_purge_and_set(
                            Attribute::AllowPrimaryCredFallback,
                            Value::new_bool(allow_primary_cred_fallback),
                        ),
                    )
                    .expect("Unable to change default session exp");
            }

            let mut e = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Uuid, Value::Uuid(target_uuid)),
                (Attribute::Name, Value::new_iname("kevin")),
                (Attribute::DisplayName, Value::new_utf8s("Kevin")),
                (Attribute::Class, EntryClass::PosixAccount.to_value()),
                (
                    Attribute::PrimaryCredential,
                    Value::Cred(
                        "primary".to_string(),
                        Credential::new_password_only(&p, "banana").unwrap()
                    )
                )
            );

            if has_posix_password {
                e.add_ava(
                    Attribute::UnixPassword,
                    Value::Cred(
                        "unix".to_string(),
                        Credential::new_password_only(&p, "kampai").unwrap(),
                    ),
                );
            }

            let ce = CreateEvent::new_internal(vec![e]);
            let cr = idms_prox_write.qs_write.create(&ce);
            assert!(cr.is_ok());
            idms_prox_write.commit().expect("Must not fail");
        }

        let result = idms
            .auth()
            .await
            .unwrap()
            .auth_ldap(
                &LdapAuthEvent {
                    target: target_uuid,
                    cleartext: if has_posix_password {
                        "kampai".to_string()
                    } else {
                        "banana".to_string()
                    },
                },
                ct,
            )
            .await;

        assert!(result.is_ok());
        if let Some(_) = expected {
            assert!(result.unwrap().is_some());
        } else {
            assert!(result.unwrap().is_none());
        }
    }

    #[idm_test]
    async fn test_idm_fallback_auth_no_pass_none_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, false, None, None).await;
    }
    #[idm_test]
    async fn test_idm_fallback_auth_pass_none_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, true, None, Some(())).await;
    }
    #[idm_test]
    async fn test_idm_fallback_auth_no_pass_true_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, false, Some(true), Some(())).await;
    }
    #[idm_test]
    async fn test_idm_fallback_auth_pass_true_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, true, Some(true), Some(())).await;
    }
    #[idm_test]
    async fn test_idm_fallback_auth_no_pass_false_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, false, Some(false), None).await;
    }
    #[idm_test]
    async fn test_idm_fallback_auth_pass_false_fallback(
        idms: &IdmServer,
        _idms_delayed: &mut IdmServerDelayed,
    ) {
        idm_fallback_auth_fixture(idms, _idms_delayed, true, Some(false), Some(())).await;
    }
}
