kanidmd_lib/idm/
serviceaccount.rs

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        // Check the classes
20        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        // qs: &mut QueryServerWriteTransaction,
60    ) -> Result<Self, OperationError> {
61        // let groups = Group::try_from_account_entry_rw(value, qs)?;
62        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        // Get the sessions.
86        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    // Who initiated this?
113    pub ident: Identity,
114    // Who is it targeting?
115    pub target: Uuid,
116}
117
118pub struct GenerateApiTokenEvent {
119    // Who initiated this?
120    pub ident: Identity,
121    // Who is it targeting?
122    pub target: Uuid,
123    // The label
124    pub label: String,
125    // When should it expire?
126    pub expiry: Option<time::OffsetDateTime>,
127    // Is it read_write capable?
128    pub read_write: bool,
129    // Limits?
130
131    // Should it be compact?
132    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    // Who initiated this?
151    pub ident: Identity,
152    // Who is it targeting?
153    pub target: Uuid,
154    // Which token id.
155    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        // Normalise to UTC in case it was provided as something else.
188        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        // create a new session
197        let session = Value::ApiToken(
198            session_id,
199            ApiToken {
200                label: gte.label.clone(),
201                expiry,
202                // Need the other inner bits?
203                // for the gracewindow.
204                issued_at,
205                // Who actually created this?
206                issued_by: gte.ident.get_event_origin_id(),
207                // What is the access scope of this session? This is
208                // for auditing purposes.
209                scope,
210            },
211        );
212
213        let token = if gte.compact {
214            // We only issue the session uuid now. This makes the token as compact as possible, fitting
215            // within 128 characters.
216            let payload = session_id.as_bytes().to_vec();
217            JwsBuilder::from(payload).build()
218        } else {
219            let purpose = scope.try_into()?;
220            // create the session token (not yet signed)
221            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        // modify the account to put the session onto it.
239        let modlist =
240            ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
241
242        self.qs_write
243            .impersonate_modify(
244                // Filter as executed
245                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
246                // Filter as intended (acp)
247                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
248                &modlist,
249                // Provide the event to impersonate
250                &gte.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        // Delete the attribute with uuid.
267        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 as executed
275                &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 as intended (acp)
283                &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                // Provide the event to impersonate
292                &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        // Generate a new random, long pw.
305        // Because this is generated, we can bypass policy checks!
306        let cleartext = password_from_random();
307        let ncred = Credential::new_generatedpassword_only(self.crypto_policy(), &cleartext)
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        // We need to remove other credentials too.
314        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        // given the new credential generate a modify
322        // We use impersonate here to get the event from ae
323        self.qs_write
324            .impersonate_modify(
325                // Filter as executed
326                &filter_all!(f_and!([
327                    f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target)),
328                    f_eq(Attribute::Class, EntryClass::ServiceAccount.into())
329                ])),
330                // Filter as intended (acp)
331                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
332                &modlist,
333                // Provide the event to impersonate
334                &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        // Make an event from the request
350        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                    // get the first entry
367                    .and_then(|e| {
368                        let account_id = e.get_uuid();
369                        // From the entry, turn it into the value
370                        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                        // No matching entry? Return none.
393                        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(&gte, ct)
446            .expect("failed to generate new api token");
447
448        trace!(?api_token);
449
450        // Deserialise it.
451        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(), Some(testaccount_uuid));
464
465        // Woohoo! Okay lets test the other edge cases.
466
467        // Check the expiry
468        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        // Delete session
476        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        // Within gracewindow?
483        // This is okay, because we are within the gracewindow.
484        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(), Some(testaccount_uuid));
488
489        // Past gracewindow?
490        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        // === request a compact token.
530        gte.compact = true;
531
532        let api_token = idms_prox_write
533            .service_account_generate_api_token(&gte, ct)
534            .expect("failed to generate new api token");
535
536        trace!(?api_token);
537
538        // Deserialise it.
539        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(), Some(testaccount_uuid));
551
552        // Woohoo! Okay lets test the other edge cases.
553
554        // Check the expiry
555        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        // Delete session
563        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        // These tokens have no grace windows, don't need to be tested.
569
570        assert!(idms_prox_write.commit().is_ok());
571    }
572}