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) => {{
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<OffsetDateTime>,
pub expire: Option<OffsetDateTime>,
pub api_tokens: BTreeMap<Uuid, ApiToken>,
pub jws_key: Option<JwsEs256Signer>,
}
impl ServiceAccount {
#[instrument(level = "debug", skip_all)]
pub(crate) fn try_from_entry_rw(
value: &Entry<EntrySealed, EntryCommitted>,
) -> Result<Self, OperationError> {
try_from_entry!(value)
}
pub(crate) fn check_api_token_valid(
ct: Duration,
apit: &ProtoApiToken,
entry: &Entry<EntrySealed, EntryCommitted>,
) -> 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;
}
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 {
pub ident: Identity,
pub target: Uuid,
}
pub struct GenerateApiTokenEvent {
pub ident: Identity,
pub target: Uuid,
pub label: String,
pub expiry: Option<time::OffsetDateTime>,
pub read_write: bool,
}
impl GenerateApiTokenEvent {
#[cfg(test)]
pub fn new_internal(target: Uuid, label: &str, expiry: Option<Duration>) -> 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 {
pub ident: Identity,
pub target: Uuid,
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<JwsCompact, OperationError> {
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;
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()?;
let session = Value::ApiToken(
session_id,
ApiToken {
label: gte.label.clone(),
expiry,
issued_at,
issued_by: gte.ident.get_event_origin_id(),
scope,
},
);
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
})?;
let modlist =
ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
self.qs_write
.impersonate_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
&modlist,
>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> {
let modlist = ModifyList::new_list(vec![Modify::Removed(
Attribute::ApiTokenSession,
PartialValue::Refer(dte.token_id),
)]);
self.qs_write
.impersonate_modify(
&filter!(f_and!([
f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
f_eq(
Attribute::ApiTokenSession,
PartialValue::Refer(dte.token_id)
)
])),
&filter_all!(f_and!([
f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
f_eq(
Attribute::ApiTokenSession,
PartialValue::Refer(dte.token_id)
)
])),
&modlist,
&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<String, OperationError> {
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);
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
.impersonate_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
&filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
&modlist,
&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<Vec<ProtoApiToken>, OperationError> {
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()
.and_then(|e| {
let account_id = e.get_uuid();
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::<Result<Vec<_>, _>>()
})
})
.unwrap_or_else(|| {
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);
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let apitoken_inner = jws_verifier
.verify(&api_token)
.unwrap()
.from_json::<ApiToken>()
.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));
assert!(
idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
.expect_err("Should not succeed")
== OperationError::SessionExpired
);
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());
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));
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());
}
}