Skip to main content

kanidmd_lib/idm/
serviceaccount.rs

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        // Check the classes
18        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        // qs: &mut QueryServerWriteTransaction,
58    ) -> Result<Self, OperationError> {
59        // let groups = Group::try_from_account_entry_rw(value, qs)?;
60        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        // Get the sessions.
84        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    // Who initiated this?
111    pub ident: Identity,
112    // Who is it targeting?
113    pub target: Uuid,
114}
115
116pub struct GenerateApiTokenEvent {
117    // Who initiated this?
118    pub ident: Identity,
119    // Who is it targeting?
120    pub target: Uuid,
121    // The label
122    pub label: String,
123    // When should it expire?
124    pub expiry: Option<time::OffsetDateTime>,
125    // Is it read_write capable?
126    pub read_write: bool,
127    // Limits?
128
129    // Should it be compact?
130    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    // Who initiated this?
149    pub ident: Identity,
150    // Who is it targeting?
151    pub target: Uuid,
152    // Which token id.
153    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        // Normalise to UTC in case it was provided as something else.
186        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        // create a new session
195        let session = Value::ApiToken(
196            session_id,
197            ApiToken {
198                label: gte.label.clone(),
199                expiry,
200                // Need the other inner bits?
201                // for the gracewindow.
202                issued_at,
203                // Who actually created this?
204                issued_by: gte.ident.get_event_origin_id(),
205                // What is the access scope of this session? This is
206                // for auditing purposes.
207                scope,
208            },
209        );
210
211        let token = if gte.compact {
212            // We only issue the session uuid now. This makes the token as compact as possible, fitting
213            // within 128 characters.
214            let payload = session_id.as_bytes().to_vec();
215            JwsBuilder::from(payload).build()
216        } else {
217            let purpose = scope.try_into()?;
218            // create the session token (not yet signed)
219            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        // modify the account to put the session onto it.
237        let modlist =
238            ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
239
240        self.qs_write
241            .impersonate_modify(
242                // Filter as executed
243                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
244                // Filter as intended (acp)
245                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
246                &modlist,
247                // Provide the event to impersonate
248                &gte.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        // Delete the attribute with uuid.
265        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 as executed
273                &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 as intended (acp)
281                &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                // Provide the event to impersonate
290                &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        // Generate a new random, long pw.
303        // Because this is generated, we can bypass policy checks!
304        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        // 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(), 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(), 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(), 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}