kanidmd_lib/idm/
serviceaccount.rs

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        // 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
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    // Who initiated this?
147    pub ident: Identity,
148    // Who is it targeting?
149    pub target: Uuid,
150    // Which token id.
151    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        // Normalise to UTC in case it was provided as something else.
184        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        // create a new session
194        let session = Value::ApiToken(
195            session_id,
196            ApiToken {
197                label: gte.label.clone(),
198                expiry,
199                // Need the other inner bits?
200                // for the gracewindow.
201                issued_at,
202                // Who actually created this?
203                issued_by: gte.ident.get_event_origin_id(),
204                // What is the access scope of this session? This is
205                // for auditing purposes.
206                scope,
207            },
208        );
209
210        // create the session token (not yet signed)
211        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        // modify the account to put the session onto it.
226        let modlist =
227            ModifyList::new_list(vec![Modify::Present(Attribute::ApiTokenSession, session)]);
228
229        self.qs_write
230            .impersonate_modify(
231                // Filter as executed
232                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
233                // Filter as intended (acp)
234                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gte.target))),
235                &modlist,
236                // Provide the event to impersonate
237                &gte.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        // Delete the attribute with uuid.
254        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 as executed
262                &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 as intended (acp)
270                &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                // Provide the event to impersonate
279                &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        // Generate a new random, long pw.
292        // Because this is generated, we can bypass policy checks!
293        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        // We need to remove other credentials too.
301        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        // given the new credential generate a modify
309        // We use impersonate here to get the event from ae
310        self.qs_write
311            .impersonate_modify(
312                // Filter as executed
313                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
314                // Filter as intended (acp)
315                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(gpe.target))),
316                &modlist,
317                // Provide the event to impersonate
318                &gpe.ident,
319            )
320            .map(|_| cleartext)
321            .map_err(|e| {
322                admin_error!("Failed to generate account password {:?}", e);
323                e
324            })
325    }
326}
327
328impl IdmServerProxyReadTransaction<'_> {
329    pub fn service_account_list_api_token(
330        &mut self,
331        lte: &ListApiTokenEvent,
332    ) -> Result<Vec<ProtoApiToken>, OperationError> {
333        // Make an event from the request
334        let srch = match SearchEvent::from_target_uuid_request(
335            lte.ident.clone(),
336            lte.target,
337            &self.qs_read,
338        ) {
339            Ok(s) => s,
340            Err(e) => {
341                admin_error!("Failed to begin service account api token list: {:?}", e);
342                return Err(e);
343            }
344        };
345
346        match self.qs_read.search_ext(&srch) {
347            Ok(mut entries) => {
348                entries
349                    .pop()
350                    // get the first entry
351                    .and_then(|e| {
352                        let account_id = e.get_uuid();
353                        // From the entry, turn it into the value
354                        e.get_ava_as_apitoken_map(Attribute::ApiTokenSession)
355                            .map(|smap| {
356                                smap.iter()
357                                    .map(|(u, s)| {
358                                        s.scope
359                                            .try_into()
360                                            .map(|purpose| ProtoApiToken {
361                                                account_id,
362                                                token_id: *u,
363                                                label: s.label.clone(),
364                                                expiry: s.expiry,
365                                                issued_at: s.issued_at,
366                                                purpose,
367                                            })
368                                            .inspect_err(|err| {
369                                                admin_error!(?err, "Invalid api_token {}", u);
370                                            })
371                                    })
372                                    .collect::<Result<Vec<_>, _>>()
373                            })
374                    })
375                    .unwrap_or_else(|| {
376                        // No matching entry? Return none.
377                        Ok(Vec::with_capacity(0))
378                    })
379            }
380            Err(e) => Err(e),
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use std::time::Duration;
388
389    use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
390    use kanidm_proto::internal::ApiToken;
391
392    use super::{DestroyApiTokenEvent, GenerateApiTokenEvent};
393    use crate::idm::server::IdmServerTransaction;
394    use crate::prelude::*;
395
396    const TEST_CURRENT_TIME: u64 = 6000;
397
398    #[idm_test]
399    async fn test_idm_service_account_api_token(
400        idms: &IdmServer,
401        _idms_delayed: &mut IdmServerDelayed,
402    ) {
403        let ct = Duration::from_secs(TEST_CURRENT_TIME);
404        let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW;
405        let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
406        let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
407        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
408
409        let testaccount_uuid = Uuid::new_v4();
410
411        let e1 = entry_init!(
412            (Attribute::Class, EntryClass::Object.to_value()),
413            (Attribute::Class, EntryClass::Account.to_value()),
414            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
415            (Attribute::Name, Value::new_iname("test_account_only")),
416            (Attribute::Uuid, Value::Uuid(testaccount_uuid)),
417            (Attribute::Description, Value::new_utf8s("testaccount")),
418            (Attribute::DisplayName, Value::new_utf8s("testaccount"))
419        );
420
421        idms_prox_write
422            .qs_write
423            .internal_create(vec![e1])
424            .expect("Failed to create service account");
425
426        let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
427
428        let api_token = idms_prox_write
429            .service_account_generate_api_token(&gte, ct)
430            .expect("failed to generate new api token");
431
432        trace!(?api_token);
433
434        // Deserialise it.
435        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
436
437        let apitoken_inner = jws_verifier
438            .verify(&api_token)
439            .unwrap()
440            .from_json::<ApiToken>()
441            .unwrap();
442
443        let ident = idms_prox_write
444            .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
445            .expect("Unable to verify api token.");
446
447        assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
448
449        // Woohoo! Okay lets test the other edge cases.
450
451        // Check the expiry
452        assert!(
453            idms_prox_write
454                .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
455                .expect_err("Should not succeed")
456                == OperationError::SessionExpired
457        );
458
459        // Delete session
460        let dte =
461            DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id);
462        assert!(idms_prox_write
463            .service_account_destroy_api_token(&dte)
464            .is_ok());
465
466        // Within gracewindow?
467        // This is okay, because we are within the gracewindow.
468        let ident = idms_prox_write
469            .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
470            .expect("Unable to verify api token.");
471        assert_eq!(ident.get_uuid(), Some(testaccount_uuid));
472
473        // Past gracewindow?
474        assert!(
475            idms_prox_write
476                .validate_client_auth_info_to_ident(api_token.into(), past_grc)
477                .expect_err("Should not succeed")
478                == OperationError::SessionExpired
479        );
480
481        assert!(idms_prox_write.commit().is_ok());
482    }
483}