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 {
session_ticket: Semaphore,
sessions: BptreeMap<Uuid, AuthSessionMutex>,
softlocks: HashMap<Uuid, CredSoftLockMutex>,
cred_update_sessions: BptreeMap<Uuid, CredentialUpdateSessionMutex>,
qs: QueryServer,
crypto_policy: CryptoPolicy,
async_tx: Sender<DelayedAction>,
audit_tx: Sender<AuditEvent>,
webauthn: Webauthn,
oauth2rs: Arc<Oauth2ResourceServers>,
applications: Arc<LdapApplications>,
}
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>,
pub(crate) sid: Sid,
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>,
pub(crate) webauthn: &'a Webauthn,
pub(crate) cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>,
pub(crate) crypto_policy: &'a CryptoPolicy,
}
pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>,
pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
}
pub struct IdmServerProxyWriteTransaction<'a> {
pub qs_write: QueryServerWriteTransaction<'a>,
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 {
CryptoPolicy::time_target(Duration::from_millis(10))
};
let (async_tx, async_rx) = unbounded();
let (audit_tx, audit_rx) = unbounded();
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(),
qs_read.get_oauth2rs_set()?,
qs_read.get_applications_set()?,
)
};
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| {
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 },
))
}
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(),
})
}
#[instrument(level = "debug", skip_all)]
pub fn domain_read(&self) -> DomainInfoRead {
self.qs.d_info.read()
}
#[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(),
})
}
#[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,
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;
#[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)
}
}
}
#[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> {
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
})?;
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));
}
};
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);
}
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 {
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);
}
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);
}
}
} 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))
}
#[instrument(level = "debug", skip_all)]
fn process_uat_to_identity(
&mut self,
uat: &UserAuthToken,
ct: Duration,
source: Source,
) -> Result<Identity, OperationError> {
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);
}
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();
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;
}
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 {
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);
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)
}
}
#[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)?;
let refers_uuid = cert_entry
.get_ava_single_refer(Attribute::Refers)
.ok_or_else(|| {
warn!("Invalid certificate entry, missing refers");
OperationError::InvalidState
})?;
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())?;
if !account.is_within_valid_time(ct) {
return Err(OperationError::SessionExpired);
};
let scope = AccessScope::ReadOnly;
let mut limits = Limits::default();
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,
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)?;
let refers_uuid = cert_entry
.get_ava_single_refer(Attribute::Refers)
.ok_or_else(|| {
warn!("Invalid certificate entry, missing refers");
OperationError::InvalidState
})?;
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())?;
if !account.is_within_valid_time(ct) {
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);
}
let anon_entry = if *uuid == UUID_ANONYMOUS {
entry
} else {
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();
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;
}
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> {
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);
}
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) {
let expire = ct - Duration::from_secs(AUTH_SESSION_TIMEOUT);
let split_at = uuid_from_duration(expire, self.sid);
let _session_ticket = self.session_ticket.acquire().await;
let mut session_write = self.sessions.write();
session_write.split_off_lt(&split_at);
session_write.commit();
}
pub async fn auth(
&mut self,
ae: &AuthEvent,
ct: Duration,
client_auth_info: ClientAuthInfo,
) -> Result<AuthResult, OperationError> {
match &ae.step {
AuthEventStep::Init(init) => {
let sessionid = uuid_from_duration(ct, self.sid);
let euuid = self.qs_read.name_to_uuid(init.username.as_str())?;
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",
);
let (account, account_policy) =
Account::try_from_entry_with_policy(entry.as_ref(), &mut self.qs_read)?;
trace!(?account.primary);
let _session_ticket = self.session_ticket.acquire().await;
let _maybe_slock_ref =
account
.primary_cred_uuid_and_policy()
.map(|(cred_uuid, policy)| {
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 {
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) {
Err(OperationError::InvalidSessionState)
} else {
session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
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::Begin(mech) => {
let session_read = self.sessions.read();
let auth_session_ref = session_read
.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;
let auth_result = auth_session.start_session(&mech.mech);
let is_valid = match auth_session.get_credential_uuid()? {
Some(cred_uuid) => {
let softlock_read = self.softlocks.read();
if let Some(slock_ref) = softlock_read.get(&cred_uuid) {
let mut slock = slock_ref.lock().await;
slock.apply_time_step(ct);
slock.is_valid()
} else {
trace!("slock not found");
false
}
}
None => true,
};
if is_valid {
auth_result
} else {
trace!("lock step begin");
auth_session.end_session("Account is temporarily locked")
}
.map(|aus| AuthResult {
sessionid: mech.sessionid,
state: aus,
})
} AuthEventStep::Cred(creds) => {
let session_read = self.sessions.read();
let auth_session_ref = session_read
.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,
};
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 {
slock.apply_time_step(ct);
slock.is_valid()
} else {
true
};
if is_valid {
auth_session
.validate_creds(
&creds.cred,
ct,
&self.async_tx,
&self.audit_tx,
self.webauthn,
self.qs_read.pw_badlist(),
)
.inspect(|aus| {
if let AuthState::Denied(_) = aus {
if let Some(ref mut slock) = maybe_slock {
slock.record_failure(ct);
}
};
})
} else {
trace!("lock step cred");
auth_session.end_session("Account is temporarily locked")
}
.map(|aus| AuthResult {
sessionid: creds.sessionid,
state: aus,
})
} }
}
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()),
};
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);
}
let valid = password.verify(cleartext).map_err(|e| {
error!(crypto_err = ?e);
e.into()
})?;
if !valid {
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)?;
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
);
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 {
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)
.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> {
if cleartext.len() < PW_MIN_LENGTH as usize {
return Err(OperationError::PasswordQuality(vec![
PasswordFeedback::TooShort(PW_MIN_LENGTH),
]));
}
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)])
})?;
if entropy.score() < 4 {
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::PasswordQuality(vec![
PasswordFeedback::BadListed,
]));
}
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> {
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
})?;
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)?;
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");
let me = self
.qs_write
.impersonate_modify_gen_event(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
&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
})?;
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> {
let account = self
.qs_write
.internal_search_uuid(pce.target)
.and_then(|account_entry| {
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
})?;
if account.unix_extn().is_none() {
return Err(OperationError::MissingClass(
ENTRYCLASS_POSIX_ACCOUNT.into(),
));
}
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");
let me = self
.qs_write
.impersonate_modify_gen_event(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(pce.target))),
&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
})?;
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
})?;
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> {
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);
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!(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)?;
let cleartext = readable_password_from_random();
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");
self.qs_write
.impersonate_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(rrse.target))),
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(rrse.target))),
&modlist,
&rrse.ident,
)
.map_err(|e| {
request_error!(error = ?e);
e
})
.map(|_| cleartext)
}
#[instrument(level = "debug", skip_all)]
fn process_pwupgrade(&mut self, pwu: &PasswordUpgrade) -> Result<(), OperationError> {
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 {
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(),
));
}
};
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)?;
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 {
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)?;
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> {
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,
issued_at: asr.issued_at,
issued_by: asr.issued_by.clone(),
cred_id: asr.cred_id,
scope: asr.scope,
type_: asr.type_,
},
);
info!(session_id = %asr.session_id, "Persisting auth session");
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
})
}
#[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))?;
self.qs_write.clear_changed_oauth2();
}
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)?;
let cleartext = readable_password_from_random();
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");
self.qs_write
.impersonate_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
&modlist,
&ev.ident,
)
.map_err(|e| {
error!(error = ?e);
e
})
.map(|_| cleartext)
}
}
#[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) {
let mut idms_auth = idms.auth().await.unwrap();
let anon_init = AuthEvent::anonymous_init();
let r1 = idms_auth
.auth(
&anon_init,
Duration::from_secs(TEST_CURRENT_TIME),
Source::Internal.into(),
)
.await;
let sid = match r1 {
Ok(ar) => {
let AuthResult { sessionid, state } = ar;
match state {
AuthState::Choose(mut conts) => {
assert_eq!(conts.len(), 1);
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!();
}
};
sessionid
}
Err(e) => {
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) => {
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);
panic!();
}
};
idms_auth.commit().expect("Must not fail");
let mut idms_auth = idms.auth().await.unwrap();
let anon_step = AuthEvent::cred_step_anonymous(sid);
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) => {
}
_ => {
error!("A critical error has occurred! We have a non-success 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_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);
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");
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)]),
);
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(_)));
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);
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) => {
token
}
_ => {
error!("A critical error has occurred! We have a non-success result!");
panic!();
}
}
}
Err(e) => {
error!("A critical error has occurred! {:?}", e);
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;
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);
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) => {
}
_ => {
error!("A critical error has occurred! We have a non-success result!");
panic!();
}
}
}
Err(e) => {
error!("A critical error has occurred! {:?}", e);
panic!();
}
};
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);
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) => {
}
_ => {
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");
}
#[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);
let r1 = idms_prox_write
.regenerate_radius_secret(&rrse)
.expect("Failed to reset radius credential 1");
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");
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();
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 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();
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);
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();
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();
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"),
};
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());
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());
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();
idms_delayed.check_is_empty_or_panic();
{
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");
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")
)]),
);
assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
assert!(idms_prox_write.commit().is_ok());
}
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);
check_testperson_password(idms, "password", ct).await;
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::PwUpgrade(_)));
let r = idms.delayed_action(duration_from_epoch_now(), da).await;
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_testperson_password(idms, "password", ct).await;
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_unix_password_upgrade(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
idms_delayed.check_is_empty_or_panic();
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();
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");
let da = idms_delayed.try_recv().expect("invalid");
let _r = idms.delayed_action(duration_from_epoch_now(), da).await;
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");
idms_delayed.check_is_empty_or_panic();
}
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));
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),
]),
);
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,
) {
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_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");
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,
) {
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 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 a1 = idms_auth.auth_unix(&uuae_good, time_low).await;
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");
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,
) {
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() {
} else {
debug_assert!(false);
}
let tok_r = idms_prox_read.get_radiusauthtoken(&rate, time_high);
if tok_r.is_err() {
} 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");
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!();
}
};
match idms_audit.audit_rx().try_recv() {
Ok(AuditEvent::AuthenticationDenied { .. }) => {}
_ => panic!("Oh no"),
}
idms_auth.commit().expect("Must not fail");
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(_)));
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");
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);
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) => {
}
_ => {
error!("A critical error has occurred! We have a non-success result!");
panic!();
}
}
}
Err(e) => {
error!("A critical error has occurred! {:?}", e);
panic!();
}
};
idms_auth.commit().expect("Must not fail");
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
idms_delayed.check_is_empty_or_panic();
}
#[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");
let sid_early =
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
let sid_later =
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_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");
let mut idms_auth = idms.auth().await.unwrap();
let anon_step = AuthEvent::cred_step_password(sid_early, TEST_PASSWORD);
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");
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"),
};
let a1 = idms_auth
.auth_unix(&uuae_good, Duration::from_secs(TEST_CURRENT_TIME))
.await;
match a1 {
Ok(None) => {}
_ => panic!("Oh no"),
};
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());
init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
let token = check_testperson_password(idms, TEST_PASSWORD, ct).await;
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);
idms_delayed.check_is_empty_or_panic();
let mut idms_prox_read = idms.proxy_read().await.unwrap();
idms_prox_read
.validate_client_auth_info_to_ident(token.clone().into(), ct)
.expect("Failed to validate");
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();
let cred_id = init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
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,
});
let r = idms.delayed_action(ct, 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!");
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);
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,
});
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(_)));
}
#[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!(post_grace < expiry);
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;
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();
idms_prox_read
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), ct)
.expect("Failed to validate");
idms_prox_read
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
.expect("Failed to validate");
drop(idms_prox_read);
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());
let mut idms_prox_read = idms.proxy_read().await.unwrap();
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);
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");
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);
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());
let mut idms_auth = idms.auth().await.unwrap();
let anon_init = AuthEvent::anonymous_init();
let r1 = idms_auth
.auth(&anon_init, ct, Source::Internal.into())
.await;
let sid = match r1 {
Ok(ar) => {
let AuthResult { sessionid, state } = ar;
match state {
AuthState::Choose(mut conts) => {
assert_eq!(conts.len(), 1);
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!();
}
};
sessionid
}
Err(e) => {
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) => {
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);
panic!();
}
};
idms_auth.commit().expect("Must not fail");
let mut idms_auth = idms.auth().await.unwrap();
let anon_step = AuthEvent::cred_step_anonymous(sid);
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);
panic!("A critical error has occurred! {:?}", e);
}
};
idms_auth.commit().expect("Must not fail");
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();
let account = idms_prox_write
.target_to_account(UUID_ADMIN)
.expect("account must exist");
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!(!ident.has_claim("authtype_anonymous"));
assert!(!ident.has_claim("authlevel_strong"));
assert!(!ident.has_claim("authclass_single"));
assert!(!ident.has_claim("authclass_mfa"));
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"));
assert!(!ident.has_claim("authlevel_strong"));
assert!(!ident.has_claim("authclass_mfa"));
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"));
assert!(!ident.has_claim("authlevel_strong"));
assert!(!ident.has_claim("authclass_mfa"));
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"));
assert!(!ident.has_claim("authclass_mfa"));
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"));
assert!(!ident.has_claim("authclass_mfa"));
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"));
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");
let account = idms_prox_write
.target_to_account(UUID_TESTPERSON_1)
.expect("account must exist");
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;
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();
idms_prox_read
.validate_client_auth_info_to_ident(token.clone().into(), ct)
.expect("Failed to validate");
drop(idms_prox_read);
let revoke_kid = token.kid().expect("token does not contain a key id");
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;
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();
assert!(idms_prox_read
.validate_client_auth_info_to_ident(token.into(), ct)
.is_err());
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();
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());
assert!(idms_prox_write
.service_account_into_person(&ident, target_uuid)
.is_ok());
}
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;
}
}