use std::collections::BTreeMap; use std::time::Duration; use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner}; use kanidm_proto::internal::ApiToken as ProtoApiToken; use time::OffsetDateTime; use crate::credential::Credential; use crate::event::SearchEvent; use crate::idm::account::Account; use crate::idm::event::GeneratePasswordEvent; use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction}; use crate::prelude::*; use crate::utils::password_from_random; use crate::value::ApiToken; macro_rules! try_from_entry { ($value:expr) => {{ // Check the classes if !$value.attribute_equality(Attribute::Class, &EntryClass::ServiceAccount.into()) { return Err(OperationError::MissingClass( ENTRYCLASS_SERVICE_ACCOUNT.into(), )); } let jws_key = $value .get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey) .cloned() .map(|jws_key| { jws_key .set_sign_option_embed_jwk(true) .set_sign_option_legacy_kid(true) }); let api_tokens = $value .get_ava_as_apitoken_map(Attribute::ApiTokenSession) .cloned() .unwrap_or_default(); let valid_from = $value.get_ava_single_datetime(Attribute::AccountValidFrom); let expire = $value.get_ava_single_datetime(Attribute::AccountExpire); let uuid = $value.get_uuid().clone(); Ok(ServiceAccount { uuid, valid_from, expire, api_tokens, jws_key, }) }}; } pub struct ServiceAccount { pub uuid: Uuid, pub valid_from: Option, pub expire: Option, pub api_tokens: BTreeMap, pub jws_key: Option, } impl ServiceAccount { #[instrument(level = "debug", skip_all)] pub(crate) fn try_from_entry_rw( value: &Entry, // qs: &mut QueryServerWriteTransaction, ) -> Result { // let groups = Group::try_from_account_entry_rw(value, qs)?; try_from_entry!(value) } pub(crate) fn check_api_token_valid( ct: Duration, apit: &ProtoApiToken, entry: &Entry, ) -> bool { 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 false; } // Get the sessions. let session_present = entry .get_ava_as_apitoken_map(Attribute::ApiTokenSession) .map(|session_map| session_map.get(&apit.token_id).is_some()) .unwrap_or(false); if session_present { security_info!("A valid session value exists for this token"); true } else { let grace = apit.issued_at + AUTH_TOKEN_GRACE_WINDOW; let current = time::OffsetDateTime::UNIX_EPOCH + ct; trace!(%grace, %current); if current >= grace { security_info!( "The token grace window has passed, and no session exists. Assuming invalid." ); false } else { security_info!("The token grace window is in effect. Assuming valid."); true } } } } pub struct ListApiTokenEvent { // Who initiated this? pub ident: Identity, // Who is it targeting? pub target: Uuid, } pub struct GenerateApiTokenEvent { // Who initiated this? pub ident: Identity, // Who is it targeting? pub target: Uuid, // The label pub label: String, // When should it expire? pub expiry: Option, // Is it read_write capable? pub read_write: bool, // Limits? } impl GenerateApiTokenEvent { #[cfg(test)] pub fn new_internal(target: Uuid, label: &str, expiry: Option) -> Self { GenerateApiTokenEvent { ident: Identity::from_internal(), target, label: label.to_string(), expiry: expiry.map(|ct| time::OffsetDateTime::UNIX_EPOCH + ct), read_write: false, } } } pub struct DestroyApiTokenEvent { // Who initiated this? pub ident: Identity, // Who is it targeting? pub target: Uuid, // Which token id. pub token_id: Uuid, } impl DestroyApiTokenEvent { #[cfg(test)] pub fn new_internal(target: Uuid, token_id: Uuid) -> Self { DestroyApiTokenEvent { ident: Identity::from_internal(), target, token_id, } } } impl<'a> IdmServerProxyWriteTransaction<'a> { pub fn service_account_generate_api_token( &mut self, gte: &GenerateApiTokenEvent, ct: Duration, ) -> Result { let service_account = self .qs_write .internal_search_uuid(gte.target) .and_then(|account_entry| ServiceAccount::try_from_entry_rw(&account_entry)) .map_err(|e| { admin_error!(?e, "Failed to search service account"); e })?; let session_id = Uuid::new_v4(); let issued_at = time::OffsetDateTime::UNIX_EPOCH + ct; // Normalise to UTC in case it was provided as something else. let expiry = gte.expiry.map(|odt| odt.to_offset(time::UtcOffset::UTC)); let scope = if gte.read_write { ApiTokenScope::ReadWrite } else { ApiTokenScope::ReadOnly }; let purpose = scope.try_into()?; // create a new session let session = Value::ApiToken( session_id, ApiToken { label: gte.label.clone(), expiry, // Need the other inner bits? // for the gracewindow. issued_at, // Who actually created this? issued_by: gte.ident.get_event_origin_id(), // What is the access scope of this session? This is // for auditing purposes. scope, }, ); // create the session token (not yet signed) let proto_api_token = ProtoApiToken { account_id: service_account.uuid, token_id: session_id, label: gte.label.clone(), expiry: gte.expiry, issued_at, purpose, }; let token = Jws::into_json(&proto_api_token).map_err(|err| { error!(?err, "Unable to serialise JWS"); OperationError::SerdeJsonError })?; // modify the account to put the session onto it. let modlist = ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]); self.qs_write .impersonate_modify( // Filter as executed &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))), // Filter as intended (acp) &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))), &modlist, // Provide the event to impersonate >e.ident, ) .map_err(|err| { error!(?err, "Failed to generate api token"); err })?; if self.qs_write.get_domain_version() < DOMAIN_LEVEL_6 { service_account .jws_key .as_ref() .ok_or_else(|| { admin_error!("Unable to sign sync token, no sync keys available"); OperationError::CryptographyError }) .and_then(|jws_key| { jws_key.sign(&token).map_err(|err| { admin_error!(?err, "Unable to sign sync token"); OperationError::CryptographyError }) }) } else { self.qs_write .get_domain_key_object_handle()? .jws_es256_sign(&token, ct) } } pub fn service_account_destroy_api_token( &mut self, dte: &DestroyApiTokenEvent, ) -> Result<(), OperationError> { // Delete the attribute with uuid. let modlist = ModifyList::new_list(vec![Modify::Removed( Attribute::ApiTokenSession, PartialValue::Refer(dte.token_id), )]); self.qs_write .impersonate_modify( // Filter as executed &filter!(f_and!([ f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)), f_eq( Attribute::ApiTokenSession, PartialValue::Refer(dte.token_id) ) ])), // Filter as intended (acp) &filter_all!(f_and!([ f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)), f_eq( Attribute::ApiTokenSession, PartialValue::Refer(dte.token_id) ) ])), &modlist, // Provide the event to impersonate &dte.ident, ) .map_err(|e| { admin_error!("Failed to destroy api token {:?}", e); e }) } pub fn generate_service_account_password( &mut self, gpe: &GeneratePasswordEvent, ) -> Result { // Generate a new random, long pw. // Because this is generated, we can bypass policy checks! let cleartext = password_from_random(); let ncred = Credential::new_generatedpassword_only(self.crypto_policy(), &cleartext) .map_err(|e| { admin_error!("Unable to generate password mod {:?}", e); e })?; let vcred = Value::new_credential("primary", ncred); // We need to remove other credentials too. let modlist = ModifyList::new_list(vec![ m_purge(Attribute::PassKeys), m_purge(Attribute::PrimaryCredential), Modify::Present(Attribute::PrimaryCredential, vcred), ]); trace!(?modlist, "processing change"); // given the new credential generate a modify // We use impersonate here to get the event from ae self.qs_write .impersonate_modify( // Filter as executed &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))), // Filter as intended (acp) &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))), &modlist, // Provide the event to impersonate &gpe.ident, ) .map(|_| cleartext) .map_err(|e| { admin_error!("Failed to generate account password {:?}", e); e }) } } impl<'a> IdmServerProxyReadTransaction<'a> { pub fn service_account_list_api_token( &mut self, lte: &ListApiTokenEvent, ) -> Result, OperationError> { // Make an event from the request let srch = match SearchEvent::from_target_uuid_request( lte.ident.clone(), lte.target, &self.qs_read, ) { Ok(s) => s, Err(e) => { admin_error!("Failed to begin service account api token list: {:?}", e); return Err(e); } }; match self.qs_read.search_ext(&srch) { Ok(mut entries) => { entries .pop() // get the first entry .and_then(|e| { let account_id = e.get_uuid(); // From the entry, turn it into the value e.get_ava_as_apitoken_map(Attribute::ApiTokenSession) .map(|smap| { smap.iter() .map(|(u, s)| { s.scope .try_into() .map(|purpose| ProtoApiToken { account_id, token_id: *u, label: s.label.clone(), expiry: s.expiry, issued_at: s.issued_at, purpose, }) .inspect_err(|err| { admin_error!(?err, "Invalid api_token {}", u); }) }) .collect::, _>>() }) }) .unwrap_or_else(|| { // No matching entry? Return none. Ok(Vec::with_capacity(0)) }) } Err(e) => Err(e), } } } #[cfg(test)] mod tests { use std::time::Duration; use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier}; use kanidm_proto::internal::ApiToken; use super::{DestroyApiTokenEvent, GenerateApiTokenEvent}; use crate::idm::server::IdmServerTransaction; use crate::prelude::*; const TEST_CURRENT_TIME: u64 = 6000; #[idm_test] async fn test_idm_service_account_api_token( idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed, ) { let ct = Duration::from_secs(TEST_CURRENT_TIME); let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW; let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000); let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010); let mut idms_prox_write = idms.proxy_write(ct).await.unwrap(); let testaccount_uuid = Uuid::new_v4(); let e1 = 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("test_account_only")), (Attribute::Uuid, Value::Uuid(testaccount_uuid)), (Attribute::Description, Value::new_utf8s("testaccount")), (Attribute::DisplayName, Value::new_utf8s("testaccount")) ); idms_prox_write .qs_write .internal_create(vec![e1]) .expect("Failed to create service account"); let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp)); let api_token = idms_prox_write .service_account_generate_api_token(>e, ct) .expect("failed to generate new api token"); trace!(?api_token); // Deserialise it. let jws_verifier = JwsDangerReleaseWithoutVerify::default(); let apitoken_inner = jws_verifier .verify(&api_token) .unwrap() .from_json::() .unwrap(); let ident = idms_prox_write .validate_client_auth_info_to_ident(api_token.clone().into(), ct) .expect("Unable to verify api token."); assert_eq!(ident.get_uuid(), Some(testaccount_uuid)); // Woohoo! Okay lets test the other edge cases. // Check the expiry assert!( idms_prox_write .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp) .expect_err("Should not succeed") == OperationError::SessionExpired ); // Delete session let dte = DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id); assert!(idms_prox_write .service_account_destroy_api_token(&dte) .is_ok()); // Within gracewindow? // This is okay, because we are within the gracewindow. let ident = idms_prox_write .validate_client_auth_info_to_ident(api_token.clone().into(), ct) .expect("Unable to verify api token."); assert_eq!(ident.get_uuid(), Some(testaccount_uuid)); // Past gracewindow? assert!( idms_prox_write .validate_client_auth_info_to_ident(api_token.into(), past_grc) .expect_err("Should not succeed") == OperationError::SessionExpired ); assert!(idms_prox_write.commit().is_ok()); } }