1use std::collections::BTreeMap;
2use std::time::Duration;
3
4use compact_jwt::{Jws, JwsCompact};
5use kanidm_proto::internal::ApiToken as ProtoApiToken;
6use time::OffsetDateTime;
7
8use crate::credential::Credential;
9use crate::event::SearchEvent;
10use crate::idm::account::Account;
11use crate::idm::event::GeneratePasswordEvent;
12use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
13use crate::prelude::*;
14use crate::utils::password_from_random;
15use crate::value::ApiToken;
16
17macro_rules! try_from_entry {
18 ($value:expr) => {{
19 if !$value.attribute_equality(Attribute::Class, &EntryClass::ServiceAccount.into()) {
21 return Err(OperationError::MissingClass(
22 ENTRYCLASS_SERVICE_ACCOUNT.into(),
23 ));
24 }
25
26 let api_tokens = $value
27 .get_ava_as_apitoken_map(Attribute::ApiTokenSession)
28 .cloned()
29 .unwrap_or_default();
30
31 let valid_from = $value.get_ava_single_datetime(Attribute::AccountValidFrom);
32
33 let expire = $value.get_ava_single_datetime(Attribute::AccountExpire);
34
35 let uuid = $value.get_uuid().clone();
36
37 Ok(ServiceAccount {
38 uuid,
39 valid_from,
40 expire,
41 api_tokens,
42 })
43 }};
44}
45
46pub struct ServiceAccount {
47 pub uuid: Uuid,
48
49 pub valid_from: Option<OffsetDateTime>,
50 pub expire: Option<OffsetDateTime>,
51
52 pub api_tokens: BTreeMap<Uuid, ApiToken>,
53}
54
55impl ServiceAccount {
56 #[instrument(level = "debug", skip_all)]
57 pub(crate) fn try_from_entry_rw(
58 value: &Entry<EntrySealed, EntryCommitted>,
59 ) -> Result<Self, OperationError> {
61 try_from_entry!(value)
63 }
64
65 pub(crate) fn check_api_token_valid(
66 ct: Duration,
67 apit: &ProtoApiToken,
68 entry: &Entry<EntrySealed, EntryCommitted>,
69 ) -> bool {
70 let within_valid_window = Account::check_within_valid_time(
71 ct,
72 entry
73 .get_ava_single_datetime(Attribute::AccountValidFrom)
74 .as_ref(),
75 entry
76 .get_ava_single_datetime(Attribute::AccountExpire)
77 .as_ref(),
78 );
79
80 if !within_valid_window {
81 security_info!("Account has expired or is not yet valid, not allowing to proceed");
82 return false;
83 }
84
85 let session_present = entry
87 .get_ava_as_apitoken_map(Attribute::ApiTokenSession)
88 .map(|session_map| session_map.get(&apit.token_id).is_some())
89 .unwrap_or(false);
90
91 if session_present {
92 security_info!("A valid session value exists for this token");
93 true
94 } else {
95 let grace = apit.issued_at + AUTH_TOKEN_GRACE_WINDOW;
96 let current = time::OffsetDateTime::UNIX_EPOCH + ct;
97 trace!(%grace, %current);
98 if current >= grace {
99 security_info!(
100 "The token grace window has passed, and no session exists. Assuming invalid."
101 );
102 false
103 } else {
104 security_info!("The token grace window is in effect. Assuming valid.");
105 true
106 }
107 }
108 }
109}
110
111pub struct ListApiTokenEvent {
112 pub ident: Identity,
114 pub target: Uuid,
116}
117
118pub struct GenerateApiTokenEvent {
119 pub ident: Identity,
121 pub target: Uuid,
123 pub label: String,
125 pub expiry: Option<time::OffsetDateTime>,
127 pub read_write: bool,
129 }
131
132impl GenerateApiTokenEvent {
133 #[cfg(test)]
134 pub fn new_internal(target: Uuid, label: &str, expiry: Option<Duration>) -> Self {
135 GenerateApiTokenEvent {
136 ident: Identity::from_internal(),
137 target,
138 label: label.to_string(),
139 expiry: expiry.map(|ct| time::OffsetDateTime::UNIX_EPOCH + ct),
140 read_write: false,
141 }
142 }
143}
144
145pub struct DestroyApiTokenEvent {
146 pub ident: Identity,
148 pub target: Uuid,
150 pub token_id: Uuid,
152}
153
154impl DestroyApiTokenEvent {
155 #[cfg(test)]
156 pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
157 DestroyApiTokenEvent {
158 ident: Identity::from_internal(),
159 target,
160 token_id,
161 }
162 }
163}
164
165impl IdmServerProxyWriteTransaction<'_> {
166 pub fn service_account_generate_api_token(
167 &mut self,
168 gte: &GenerateApiTokenEvent,
169 ct: Duration,
170 ) -> Result<JwsCompact, OperationError> {
171 let service_account = self
172 .qs_write
173 .internal_search_uuid(gte.target)
174 .and_then(|account_entry| ServiceAccount::try_from_entry_rw(&account_entry))
175 .map_err(|e| {
176 admin_error!(?e, "Failed to search service account");
177 e
178 })?;
179
180 let session_id = Uuid::new_v4();
181 let issued_at = time::OffsetDateTime::UNIX_EPOCH + ct;
182
183 let expiry = gte.expiry.map(|odt| odt.to_offset(time::UtcOffset::UTC));
185
186 let scope = if gte.read_write {
187 ApiTokenScope::ReadWrite
188 } else {
189 ApiTokenScope::ReadOnly
190 };
191 let purpose = scope.try_into()?;
192
193 let session = Value::ApiToken(
195 session_id,
196 ApiToken {
197 label: gte.label.clone(),
198 expiry,
199 issued_at,
202 issued_by: gte.ident.get_event_origin_id(),
204 scope,
207 },
208 );
209
210 let proto_api_token = ProtoApiToken {
212 account_id: service_account.uuid,
213 token_id: session_id,
214 label: gte.label.clone(),
215 expiry: gte.expiry,
216 issued_at,
217 purpose,
218 };
219
220 let token = Jws::into_json(&proto_api_token).map_err(|err| {
221 error!(?err, "Unable to serialise JWS");
222 OperationError::SerdeJsonError
223 })?;
224
225 let modlist =
227 ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
228
229 self.qs_write
230 .impersonate_modify(
231 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
233 &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
235 &modlist,
236 >e.ident,
238 )
239 .map_err(|err| {
240 error!(?err, "Failed to generate api token");
241 err
242 })?;
243
244 self.qs_write
245 .get_domain_key_object_handle()?
246 .jws_es256_sign(&token, ct)
247 }
248
249 pub fn service_account_destroy_api_token(
250 &mut self,
251 dte: &DestroyApiTokenEvent,
252 ) -> Result<(), OperationError> {
253 let modlist = ModifyList::new_list(vec![Modify::Removed(
255 Attribute::ApiTokenSession,
256 PartialValue::Refer(dte.token_id),
257 )]);
258
259 self.qs_write
260 .impersonate_modify(
261 &filter!(f_and!([
263 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
264 f_eq(
265 Attribute::ApiTokenSession,
266 PartialValue::Refer(dte.token_id)
267 )
268 ])),
269 &filter_all!(f_and!([
271 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
272 f_eq(
273 Attribute::ApiTokenSession,
274 PartialValue::Refer(dte.token_id)
275 )
276 ])),
277 &modlist,
278 &dte.ident,
280 )
281 .map_err(|e| {
282 admin_error!("Failed to destroy api token {:?}", e);
283 e
284 })
285 }
286
287 pub fn generate_service_account_password(
288 &mut self,
289 gpe: &GeneratePasswordEvent,
290 ) -> Result<String, OperationError> {
291 let cleartext = password_from_random();
294 let ncred = Credential::new_generatedpassword_only(self.crypto_policy(), &cleartext)
295 .map_err(|e| {
296 admin_error!("Unable to generate password mod {:?}", e);
297 e
298 })?;
299 let vcred = Value::new_credential("primary", ncred);
300 let modlist = ModifyList::new_list(vec![
302 m_purge(Attribute::PassKeys),
303 m_purge(Attribute::PrimaryCredential),
304 Modify::Present(Attribute::PrimaryCredential, vcred),
305 ]);
306
307 trace!(?modlist, "processing change");
308 self.qs_write
311 .impersonate_modify(
312 &filter_all!(f_and!([
314 f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target)),
315 f_eq(Attribute::Class, EntryClass::ServiceAccount.into())
316 ])),
317 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
319 &modlist,
320 &gpe.ident,
322 )
323 .map(|_| cleartext)
324 .map_err(|e| {
325 admin_error!("Failed to generate account password {:?}", e);
326 e
327 })
328 }
329}
330
331impl IdmServerProxyReadTransaction<'_> {
332 pub fn service_account_list_api_token(
333 &mut self,
334 lte: &ListApiTokenEvent,
335 ) -> Result<Vec<ProtoApiToken>, OperationError> {
336 let srch = match SearchEvent::from_target_uuid_request(
338 lte.ident.clone(),
339 lte.target,
340 &self.qs_read,
341 ) {
342 Ok(s) => s,
343 Err(e) => {
344 admin_error!("Failed to begin service account api token list: {:?}", e);
345 return Err(e);
346 }
347 };
348
349 match self.qs_read.search_ext(&srch) {
350 Ok(mut entries) => {
351 entries
352 .pop()
353 .and_then(|e| {
355 let account_id = e.get_uuid();
356 e.get_ava_as_apitoken_map(Attribute::ApiTokenSession)
358 .map(|smap| {
359 smap.iter()
360 .map(|(u, s)| {
361 s.scope
362 .try_into()
363 .map(|purpose| ProtoApiToken {
364 account_id,
365 token_id: *u,
366 label: s.label.clone(),
367 expiry: s.expiry,
368 issued_at: s.issued_at,
369 purpose,
370 })
371 .inspect_err(|err| {
372 admin_error!(?err, "Invalid api_token {}", u);
373 })
374 })
375 .collect::<Result<Vec<_>, _>>()
376 })
377 })
378 .unwrap_or_else(|| {
379 Ok(Vec::with_capacity(0))
381 })
382 }
383 Err(e) => Err(e),
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use std::time::Duration;
391
392 use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
393 use kanidm_proto::internal::ApiToken;
394
395 use super::{DestroyApiTokenEvent, GenerateApiTokenEvent};
396 use crate::idm::server::IdmServerTransaction;
397 use crate::prelude::*;
398
399 const TEST_CURRENT_TIME: u64 = 6000;
400
401 #[idm_test]
402 async fn test_idm_service_account_api_token(
403 idms: &IdmServer,
404 _idms_delayed: &mut IdmServerDelayed,
405 ) {
406 let ct = Duration::from_secs(TEST_CURRENT_TIME);
407 let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW;
408 let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
409 let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
410 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
411
412 let testaccount_uuid = Uuid::new_v4();
413
414 let e1 = entry_init!(
415 (Attribute::Class, EntryClass::Object.to_value()),
416 (Attribute::Class, EntryClass::Account.to_value()),
417 (Attribute::Class, EntryClass::ServiceAccount.to_value()),
418 (Attribute::Name, Value::new_iname("test_account_only")),
419 (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
420 (Attribute::Description, Value::new_utf8s("testaccount")),
421 (Attribute::DisplayName, Value::new_utf8s("testaccount"))
422 );
423
424 idms_prox_write
425 .qs_write
426 .internal_create(vec![e1])
427 .expect("Failed to create service account");
428
429 let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
430
431 let api_token = idms_prox_write
432 .service_account_generate_api_token(>e, ct)
433 .expect("failed to generate new api token");
434
435 trace!(?api_token);
436
437 let jws_verifier = JwsDangerReleaseWithoutVerify::default();
439
440 let apitoken_inner = jws_verifier
441 .verify(&api_token)
442 .unwrap()
443 .from_json::<ApiToken>()
444 .unwrap();
445
446 let ident = idms_prox_write
447 .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
448 .expect("Unable to verify api token.");
449
450 assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
451
452 assert!(
456 idms_prox_write
457 .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
458 .expect_err("Should not succeed")
459 == OperationError::SessionExpired
460 );
461
462 let dte =
464 DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id);
465 assert!(idms_prox_write
466 .service_account_destroy_api_token(&dte)
467 .is_ok());
468
469 let ident = idms_prox_write
472 .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
473 .expect("Unable to verify api token.");
474 assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
475
476 assert!(
478 idms_prox_write
479 .validate_client_auth_info_to_ident(api_token.into(), past_grc)
480 .expect_err("Should not succeed")
481 == OperationError::SessionExpired
482 );
483
484 assert!(idms_prox_write.commit().is_ok());
485 }
486}