1use std::collections::BTreeMap;
2use std::time::Duration;
3
4use compact_jwt::{jws::JwsBuilder, 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 pub compact: bool,
133}
134
135impl GenerateApiTokenEvent {
136 #[cfg(test)]
137 pub fn new_internal(target: Uuid, label: &str, expiry: Option<Duration>) -> Self {
138 GenerateApiTokenEvent {
139 ident: Identity::from_internal(),
140 target,
141 label: label.to_string(),
142 expiry: expiry.map(|ct| time::OffsetDateTime::UNIX_EPOCH + ct),
143 read_write: false,
144 compact: false,
145 }
146 }
147}
148
149pub struct DestroyApiTokenEvent {
150 pub ident: Identity,
152 pub target: Uuid,
154 pub token_id: Uuid,
156}
157
158impl DestroyApiTokenEvent {
159 #[cfg(test)]
160 pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
161 DestroyApiTokenEvent {
162 ident: Identity::from_internal(),
163 target,
164 token_id,
165 }
166 }
167}
168
169impl IdmServerProxyWriteTransaction<'_> {
170 pub fn service_account_generate_api_token(
171 &mut self,
172 gte: &GenerateApiTokenEvent,
173 ct: Duration,
174 ) -> Result<JwsCompact, OperationError> {
175 let service_account = self
176 .qs_write
177 .internal_search_uuid(gte.target)
178 .and_then(|account_entry| ServiceAccount::try_from_entry_rw(&account_entry))
179 .map_err(|e| {
180 admin_error!(?e, "Failed to search service account");
181 e
182 })?;
183
184 let session_id = Uuid::new_v4();
185 let issued_at = time::OffsetDateTime::UNIX_EPOCH + ct;
186
187 let expiry = gte.expiry.map(|odt| odt.to_offset(time::UtcOffset::UTC));
189
190 let scope = if gte.read_write {
191 ApiTokenScope::ReadWrite
192 } else {
193 ApiTokenScope::ReadOnly
194 };
195
196 let session = Value::ApiToken(
198 session_id,
199 ApiToken {
200 label: gte.label.clone(),
201 expiry,
202 issued_at,
205 issued_by: gte.ident.get_event_origin_id(),
207 scope,
210 },
211 );
212
213 let token = if gte.compact {
214 let payload = session_id.as_bytes().to_vec();
217 JwsBuilder::from(payload).build()
218 } else {
219 let purpose = scope.try_into()?;
220 let proto_api_token = ProtoApiToken {
222 account_id: service_account.uuid,
223 token_id: session_id,
224 label: gte.label.clone(),
225 expiry: gte.expiry,
226 issued_at,
227 purpose,
228 };
229
230 let token = Jws::into_json(&proto_api_token).map_err(|err| {
231 error!(?err, "Unable to serialise JWS");
232 OperationError::SerdeJsonError
233 })?;
234
235 token
236 };
237
238 let modlist =
240 ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
241
242 self.qs_write
243 .impersonate_modify(
244 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
246 &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
248 &modlist,
249 >e.ident,
251 )
252 .map_err(|err| {
253 error!(?err, "Failed to generate api token");
254 err
255 })?;
256
257 self.qs_write
258 .get_domain_key_object_handle()?
259 .jws_hs256_sign(&token, ct)
260 }
261
262 pub fn service_account_destroy_api_token(
263 &mut self,
264 dte: &DestroyApiTokenEvent,
265 ) -> Result<(), OperationError> {
266 let modlist = ModifyList::new_list(vec![Modify::Removed(
268 Attribute::ApiTokenSession,
269 PartialValue::Refer(dte.token_id),
270 )]);
271
272 self.qs_write
273 .impersonate_modify(
274 &filter!(f_and!([
276 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
277 f_eq(
278 Attribute::ApiTokenSession,
279 PartialValue::Refer(dte.token_id)
280 )
281 ])),
282 &filter_all!(f_and!([
284 f_eq(Attribute::Uuid, PartialValue::Uuid(dte.target)),
285 f_eq(
286 Attribute::ApiTokenSession,
287 PartialValue::Refer(dte.token_id)
288 )
289 ])),
290 &modlist,
291 &dte.ident,
293 )
294 .map_err(|e| {
295 admin_error!("Failed to destroy api token {:?}", e);
296 e
297 })
298 }
299
300 pub fn generate_service_account_password(
301 &mut self,
302 gpe: &GeneratePasswordEvent,
303 ) -> Result<String, OperationError> {
304 let cleartext = password_from_random();
307 let timestamp = self.qs_write.get_curtime_odt();
308 let ncred =
309 Credential::new_generatedpassword_only(self.crypto_policy(), &cleartext, timestamp)
310 .map_err(|e| {
311 admin_error!("Unable to generate password mod {:?}", e);
312 e
313 })?;
314 let vcred = Value::new_credential("primary", ncred);
315 let modlist = ModifyList::new_list(vec![
317 m_purge(Attribute::PassKeys),
318 m_purge(Attribute::PrimaryCredential),
319 Modify::Present(Attribute::PrimaryCredential, vcred),
320 ]);
321
322 trace!(?modlist, "processing change");
323 self.qs_write
326 .impersonate_modify(
327 &filter_all!(f_and!([
329 f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target)),
330 f_eq(Attribute::Class, EntryClass::ServiceAccount.into())
331 ])),
332 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
334 &modlist,
335 &gpe.ident,
337 )
338 .map(|_| cleartext)
339 .map_err(|e| {
340 admin_error!("Failed to generate account password {:?}", e);
341 e
342 })
343 }
344}
345
346impl IdmServerProxyReadTransaction<'_> {
347 pub fn service_account_list_api_token(
348 &mut self,
349 lte: &ListApiTokenEvent,
350 ) -> Result<Vec<ProtoApiToken>, OperationError> {
351 let srch = match SearchEvent::from_target_uuid_request(
353 lte.ident.clone(),
354 lte.target,
355 &self.qs_read,
356 ) {
357 Ok(s) => s,
358 Err(e) => {
359 admin_error!("Failed to begin service account api token list: {:?}", e);
360 return Err(e);
361 }
362 };
363
364 match self.qs_read.search_ext(&srch) {
365 Ok(mut entries) => {
366 entries
367 .pop()
368 .and_then(|e| {
370 let account_id = e.get_uuid();
371 e.get_ava_as_apitoken_map(Attribute::ApiTokenSession)
373 .map(|smap| {
374 smap.iter()
375 .map(|(u, s)| {
376 s.scope
377 .try_into()
378 .map(|purpose| ProtoApiToken {
379 account_id,
380 token_id: *u,
381 label: s.label.clone(),
382 expiry: s.expiry,
383 issued_at: s.issued_at,
384 purpose,
385 })
386 .inspect_err(|err| {
387 admin_error!(?err, "Invalid api_token {}", u);
388 })
389 })
390 .collect::<Result<Vec<_>, _>>()
391 })
392 })
393 .unwrap_or_else(|| {
394 Ok(Vec::with_capacity(0))
396 })
397 }
398 Err(e) => Err(e),
399 }
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use std::time::Duration;
406
407 use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
408 use kanidm_proto::internal::ApiToken;
409
410 use super::{DestroyApiTokenEvent, GenerateApiTokenEvent};
411 use crate::idm::server::IdmServerTransaction;
412 use crate::prelude::*;
413
414 const TEST_CURRENT_TIME: u64 = 6000;
415
416 #[idm_test]
417 async fn test_idm_service_account_api_token(
418 idms: &IdmServer,
419 _idms_delayed: &mut IdmServerDelayed,
420 ) {
421 let ct = Duration::from_secs(TEST_CURRENT_TIME);
422 let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW;
423 let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
424 let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
425 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
426
427 let testaccount_uuid = Uuid::new_v4();
428
429 let e1 = entry_init!(
430 (Attribute::Class, EntryClass::Object.to_value()),
431 (Attribute::Class, EntryClass::Account.to_value()),
432 (Attribute::Class, EntryClass::ServiceAccount.to_value()),
433 (Attribute::Name, Value::new_iname("test_account_only")),
434 (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
435 (Attribute::Description, Value::new_utf8s("testaccount")),
436 (Attribute::DisplayName, Value::new_utf8s("testaccount"))
437 );
438
439 idms_prox_write
440 .qs_write
441 .internal_create(vec![e1])
442 .expect("Failed to create service account");
443
444 let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
445
446 let api_token = idms_prox_write
447 .service_account_generate_api_token(>e, ct)
448 .expect("failed to generate new api token");
449
450 trace!(?api_token);
451
452 let jws_verifier = JwsDangerReleaseWithoutVerify::default();
454
455 let apitoken_inner = jws_verifier
456 .verify(&api_token)
457 .unwrap()
458 .from_json::<ApiToken>()
459 .unwrap();
460
461 let ident = idms_prox_write
462 .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
463 .expect("Unable to verify api token.");
464
465 assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
466
467 assert!(
471 idms_prox_write
472 .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
473 .expect_err("Should not succeed")
474 == OperationError::SessionExpired
475 );
476
477 let dte =
479 DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id);
480 assert!(idms_prox_write
481 .service_account_destroy_api_token(&dte)
482 .is_ok());
483
484 let ident = idms_prox_write
487 .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
488 .expect("Unable to verify api token.");
489 assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
490
491 assert!(
493 idms_prox_write
494 .validate_client_auth_info_to_ident(api_token.into(), past_grc)
495 .expect_err("Should not succeed")
496 == OperationError::SessionExpired
497 );
498
499 assert!(idms_prox_write.commit().is_ok());
500 }
501
502 #[idm_test]
503 async fn test_idm_service_account_compact_api_token(
504 idms: &IdmServer,
505 _idms_delayed: &mut IdmServerDelayed,
506 ) {
507 let ct = Duration::from_secs(TEST_CURRENT_TIME);
508 let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
509 let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
510 let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
511
512 let testaccount_uuid = Uuid::new_v4();
513
514 let e1 = entry_init!(
515 (Attribute::Class, EntryClass::Object.to_value()),
516 (Attribute::Class, EntryClass::Account.to_value()),
517 (Attribute::Class, EntryClass::ServiceAccount.to_value()),
518 (Attribute::Name, Value::new_iname("test_account_only")),
519 (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
520 (Attribute::Description, Value::new_utf8s("testaccount")),
521 (Attribute::DisplayName, Value::new_utf8s("testaccount"))
522 );
523
524 idms_prox_write
525 .qs_write
526 .internal_create(vec![e1])
527 .expect("Failed to create service account");
528
529 let mut gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
530
531 gte.compact = true;
533
534 let api_token = idms_prox_write
535 .service_account_generate_api_token(>e, ct)
536 .expect("failed to generate new api token");
537
538 trace!(?api_token);
539
540 let jws_verifier = JwsDangerReleaseWithoutVerify::default();
542
543 let apitoken_inner = jws_verifier.verify(&api_token).unwrap();
544
545 let session_id = Uuid::from_slice(apitoken_inner.payload())
546 .expect("Unable to decode compact token as session id");
547
548 let ident = idms_prox_write
549 .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
550 .expect("Unable to verify api token.");
551
552 assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
553
554 assert!(
558 idms_prox_write
559 .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
560 .expect_err("Should not succeed")
561 == OperationError::SessionExpired
562 );
563
564 let dte = DestroyApiTokenEvent::new_internal(testaccount_uuid, session_id);
566 assert!(idms_prox_write
567 .service_account_destroy_api_token(&dte)
568 .is_ok());
569
570 assert!(idms_prox_write.commit().is_ok());
573 }
574}