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