Skip to main content

pubhubs/servers/phc/
user.rs

1//! Basic user endpoints, such as [`EnterEP`].
2use crate::api;
3use crate::api::OpenError;
4use crate::attr::{Attr, AttrState};
5use crate::common::elgamal;
6use crate::common::secret::DigestibleSecret as _;
7use crate::handle;
8use crate::hub;
9use crate::id::Id;
10use crate::misc::crypto;
11use crate::misc::error::{OPAQUE, Opaque};
12use crate::misc::jwt;
13
14use std::collections::{HashMap, HashSet};
15use std::ops::Deref;
16use std::rc::Rc;
17
18use actix_web::web;
19use sha2::digest::Digest as _;
20
21use super::server::*;
22use api::phc::user::*;
23
24use api::phc::user::UserState as ApiUserState;
25
26impl App {
27    /// Implements [`WelcomeEP`]
28    pub(super) fn cached_handle_user_welcome(app: &Self) -> api::Result<WelcomeResp> {
29        let running_state = app.running_state_or_please_retry()?;
30
31        let hubs: HashMap<handle::Handle, hub::BasicInfo> = app
32            .hubs
33            .values()
34            .map(|hub| (hub.handles.preferred().clone(), hub.clone()))
35            .collect();
36
37        Ok(WelcomeResp {
38            constellation: (*running_state.constellation).clone(),
39            hubs,
40        })
41    }
42
43    /// Implements [`CachedHubInfoEP`]
44    pub(super) async fn handle_cached_hub_info(
45        app: web::Data<Rc<App>>,
46    ) -> impl actix_web::Responder {
47        app.cached_hub_info.borrow().clone()
48    }
49
50    /// Implements [`StateEP`]
51    pub(super) async fn handle_user_state(
52        app: Rc<Self>,
53        auth_token: actix_web::web::Header<AuthToken>,
54    ) -> api::Result<StateResp> {
55        let Ok((user_state, _)) = app
56            .open_auth_token_and_get_user_state(auth_token.into_inner())
57            .await?
58        else {
59            return Ok(StateResp::RetryWithNewAuthToken);
60        };
61
62        Ok(StateResp::State(user_state.into_user_version(&app)))
63    }
64
65    /// Implements [`EnterEP`]
66    pub(super) async fn handle_user_enter(
67        app: Rc<Self>,
68        req: web::Json<EnterReq>,
69        auth_token: Option<actix_web::web::Header<AuthToken>>,
70    ) -> api::Result<EnterResp> {
71        let running_state = &app.running_state_or_please_retry()?;
72
73        let EnterReq {
74            identifying_attr,
75            mode,
76            add_attrs,
77            register_only_with_unique_attrs,
78        } = req.into_inner();
79
80        let auth_token_user_id = if let Some(auth_token) = auth_token {
81            let Ok(user_id) = app.open_auth_token(auth_token.into_inner()) else {
82                return Ok(EnterResp::RetryWithNewAuthToken);
83            };
84
85            if !matches!(mode, EnterMode::Login) {
86                log::debug!("a user tried to enter with an auth token, but not in the login mode");
87                return Err(api::ErrorCode::BadRequest);
88            }
89
90            Some(user_id)
91        } else {
92            None
93        };
94
95        if auth_token_user_id.is_none() && identifying_attr.is_none() {
96            log::debug!("entry request with neither auth token nor identifying attribute");
97            return Err(api::ErrorCode::BadRequest);
98        }
99
100        if register_only_with_unique_attrs {
101            match mode {
102                EnterMode::Login => {
103                    log::debug!(
104                        "entry request with `register_only_with_unique_attrs` set, but mode `Login`"
105                    );
106                }
107                EnterMode::Register | EnterMode::LoginOrRegister => { /* Ok */ }
108            }
109        }
110
111        // Check attributes are valid
112        let identifying_attr = if let Some(identifying_attr) = identifying_attr {
113            let identifying_attr = app.id_attr(
114                match identifying_attr.open(&running_state.attr_signing_key, None) {
115                    Ok(identifying_attr) => identifying_attr,
116                    Err(OpenError::OtherConstellation(..)) | Err(OpenError::InternalError) => {
117                        return Err(api::ErrorCode::InternalError);
118                    }
119                    Err(OpenError::OtherwiseInvalid) => {
120                        return Err(api::ErrorCode::BadRequest);
121                    }
122                    Err(OpenError::Expired) | Err(OpenError::InvalidSignature) => {
123                        return Ok(EnterResp::RetryWithNewIdentifyingAttr);
124                    }
125                },
126            );
127
128            if identifying_attr.not_identifying {
129                log::warn!(
130                    "supposed attribute {} of type {} is not identifying",
131                    identifying_attr.value,
132                    identifying_attr.attr_type
133                );
134                return Err(api::ErrorCode::BadRequest);
135            }
136
137            Some(identifying_attr)
138        } else {
139            None
140        };
141
142        let attrs: HashMap<Id, IdedAttr> = {
143            let mut attrs: HashMap<Id, IdedAttr> = HashMap::with_capacity(add_attrs.len());
144
145            if let Some(ref identifying_attr) = identifying_attr {
146                attrs.insert(identifying_attr.id, identifying_attr.clone());
147            }
148
149            for (add_attr_index, add_attr) in add_attrs.into_iter().enumerate() {
150                let ided_attr =
151                    app.id_attr(match add_attr.open(&running_state.attr_signing_key, None) {
152                        Ok(attr) => attr,
153                        Err(OpenError::OtherConstellation(..)) | Err(OpenError::InternalError) => {
154                            return Err(api::ErrorCode::InternalError);
155                        }
156                        Err(OpenError::OtherwiseInvalid) => {
157                            return Err(api::ErrorCode::BadRequest);
158                        }
159                        Err(OpenError::Expired) | Err(OpenError::InvalidSignature) => {
160                            return Ok(EnterResp::RetryWithNewAddAttr {
161                                index: add_attr_index,
162                            });
163                        }
164                    });
165
166                if ided_attr.not_addable {
167                    log::warn!(
168                        "entry: someone tried to add unaddable attribute of type {}",
169                        ided_attr.attr_type
170                    );
171                    return Err(api::ErrorCode::BadRequest);
172                }
173
174                let previous_value = attrs.insert(ided_attr.id, ided_attr);
175
176                if let Some(attr) = previous_value {
177                    log::warn!(
178                        "entry: attribute {} of type {} provided twice",
179                        attr.value,
180                        attr.attr_type
181                    );
182                    return Err(api::ErrorCode::BadRequest);
183                }
184            }
185
186            attrs
187        };
188
189        // will be filled while getting and putting attributes to the object store
190        let mut attr_states: std::collections::HashMap<
191            Id,
192            (AttrState, object_store::UpdateVersion),
193        > = Default::default();
194
195        // Items are added to `attr_state` incidentally until at some point we loop over all
196        // attributes in attrs that are not yet in `attr_states`.  When this happens depends on
197        // whether the user account already exists or not. To keep track of whether it happened
198        // we've added the following boolean.
199        let mut retrieved_attr_states = false;
200
201        // keeps track of which attributes have already been added
202        let mut attr_add_status: HashMap<Id, AttrAddStatus> = Default::default();
203
204        // Attributes are fine, check if we have a user account, or create it if need be
205        let ((user_state, mut user_state_version), new_account) = 'found_user: {
206            if let Some(auth_token_user_id) = auth_token_user_id {
207                let user_and_version = app
208                    .get_object::<UserState>(&auth_token_user_id)
209                    .await?
210                    .ok_or_else(|| {
211                        log::error!(
212                            "a valid auth token passed during entry refers to a user with user_id {auth_token_user_id}  that does not exist",
213                        );
214                        api::ErrorCode::InternalError
215                    })?;
216
217                break 'found_user (user_and_version, false);
218            }
219
220            let identifying_attr = identifying_attr.expect(
221                "we should (but don't) have either an auth token or an identifying attribute",
222            );
223
224            if matches!(mode, EnterMode::Login | EnterMode::LoginOrRegister) {
225                // see if account exists
226                if let Some((ias, ias_v)) =
227                    app.get_object::<AttrState>(&identifying_attr.id).await?
228                {
229                    log::trace!(
230                        "enter: account exists for attribute {} of type {}",
231                        identifying_attr.value,
232                        identifying_attr.attr_type,
233                    );
234
235                    let user_id = ias.may_identify_user.ok_or_else(|| {
236                        log::error!(
237                            "identifying attribute {} of type {} has may_identify_user set to None",
238                            identifying_attr.value,
239                            identifying_attr.attr_type
240                        );
241                        api::ErrorCode::InternalError
242                    })?;
243
244                    attr_states.insert(identifying_attr.id, (ias, ias_v));
245
246                    let user_and_version = app
247                        .get_object::<UserState>(&user_id)
248                        .await?
249                        .ok_or_else(|| {
250                            log::error!(
251                                "identifying attribute {} of type {} refers to a user \
252                            account {user_id} that does not exist",
253                                identifying_attr.value,
254                                identifying_attr.attr_type
255                            );
256                            api::ErrorCode::InternalError
257                        })?;
258
259                    break 'found_user (user_and_version, false);
260                }
261
262                log::trace!(
263                    "enter: no account exists for attribute {} of type {}",
264                    identifying_attr.value,
265                    identifying_attr.attr_type,
266                );
267            }
268
269            if mode == EnterMode::Login {
270                return Ok(EnterResp::AccountDoesNotExist);
271            }
272
273            assert!(matches!(
274                mode,
275                EnterMode::LoginOrRegister | EnterMode::Register
276            ));
277
278            if let Some(resp) = app
279                .precheck_attrs_for_registration(
280                    &attrs,
281                    &mut attr_states,
282                    &mut retrieved_attr_states,
283                    register_only_with_unique_attrs,
284                )
285                .await?
286            {
287                return Ok(resp);
288            }
289
290            // we need to be careful with the order of things here lest we leave the object
291            // store in a broken state.
292            //
293            //  1. Add the user account object.  Include the identifying attributes in the user
294            //     account already - they can be added later - but do not include the bannable
295            //     attributes. If this fails, the client just needs to register
296            //     again.
297            //
298            //  2. Add the identifying attribute pointing to the user account.  If this fails, the
299            //     client can always register again, and we're only left with an orphaned account.
300            //
301            //  3. Add the other attributes.  If this fails the user can always add the attributes
302            //     again using the identifying attribute already registered.
303            //
304            //  4. Modify the user account to register the added bannable attributes.  If this
305            //     fails, the user can always re-add those bannable attributes.
306            //
307            //  Here, we're only doing steps 1 and 2. Steps 3 and 4 are shared with regular login.
308
309            let user_state = UserState {
310                id: Id::random(),
311                card_id: Some(CardPseud(Id::random())),
312                registration_date: Some(api::NumericDate::now()),
313                polymorphic_pseudonym: running_state.constellation.master_enc_key.encrypt_random(),
314                banned: false,
315                allow_login_by: attrs
316                    .values()
317                    .filter_map(|attr| {
318                        if attr.not_identifying {
319                            None
320                        } else {
321                            Some(attr.id)
322                        }
323                    })
324                    .collect(),
325                could_be_banned_by: Default::default(),
326                // NOTE: `could_be_banned_by` is set after the bannable attributes have been added
327                stored_objects: Default::default(),
328            };
329
330            let user_state_version = app
331                .put_object::<UserState>(&user_state, None)
332                .await?
333                .ok_or_else(|| {
334                    log::error!("User with id {} already exists - very odd", user_state.id);
335                    api::ErrorCode::InternalError
336                })?;
337
338            // Add identifying attribute.  We do not expect the attribute to exist, because
339            // otherwise we would be logging in, not registering.
340            assert!(!attr_states.contains_key(&identifying_attr.id));
341
342            let identifying_attr_state =
343                AttrState::new(identifying_attr.id, &identifying_attr, user_state.id);
344
345            if let Some(identifying_attr_state_version) = app
346                .put_object::<AttrState>(&identifying_attr_state, None)
347                .await
348                .inspect_err(|err| {
349                    log::warn!(
350                        "orphaned user account {} due to error with putting \
351                        identifying attribute: {err}",
352                        user_state.id
353                    );
354                })?
355            {
356                assert!(
357                    attr_states
358                        .insert(
359                            identifying_attr.id,
360                            (identifying_attr_state, identifying_attr_state_version),
361                        )
362                        .is_none()
363                );
364
365                assert!(
366                    attr_add_status
367                        .insert(identifying_attr.id, AttrAddStatus::Added)
368                        .is_none()
369                );
370            } else {
371                log::warn!(
372                    "possibly orphaned user account {} because identifying \
373                    attribute {} was just added before our noses",
374                    user_state.id,
375                    identifying_attr.id
376                );
377                return Ok(EnterResp::AttributeAlreadyTaken {
378                    attr: identifying_attr.attr,
379                    bans_other_user: false,
380                });
381            }
382
383            log::debug!("created user account {}", user_state.id);
384            break 'found_user ((user_state, user_state_version), true);
385        };
386
387        if user_state.banned {
388            return Ok(EnterResp::Banned);
389        }
390
391        if !retrieved_attr_states {
392            for attr in attrs.values() {
393                if attr_states.contains_key(&attr.id) {
394                    continue;
395                }
396
397                if let Some(attr_state_and_version) = app.get_object::<AttrState>(&attr.id).await? {
398                    attr_states.insert(attr.id, attr_state_and_version);
399                }
400            }
401
402            retrieved_attr_states = true;
403        }
404
405        assert!(retrieved_attr_states);
406
407        // Add the missing attributes.  First the attribute states.
408        for attr in attrs.values() {
409            if attr_states.contains_key(&attr.id) {
410                attr_add_status
411                    .entry(attr.id)
412                    .or_insert(AttrAddStatus::AlreadyThere);
413                continue;
414            }
415
416            let attr_state = AttrState::new(attr.id, attr, user_state.id);
417
418            match app.put_object::<AttrState>(&attr_state, None).await {
419                Ok(Some(attr_state_version)) => {
420                    assert!(
421                        attr_states
422                            .insert(attr.id, (attr_state, attr_state_version))
423                            .is_none()
424                    );
425                    assert!(
426                        attr_add_status
427                            .insert(attr.id, AttrAddStatus::Added)
428                            .is_none()
429                    );
430                }
431                problem => {
432                    log::warn!("problem adding attribute state {}: {problem:?}", attr.value);
433                    assert!(
434                        attr_add_status
435                            .insert(attr.id, AttrAddStatus::PleaseTryAgain)
436                            .is_none()
437                    );
438                }
439            }
440        }
441
442        // Now check that all the bannable attributes ban this user
443        for attr in attrs.values() {
444            let (attr_state, attr_state_version) =
445                if let Some(attr_state_and_version) = attr_states.get(&attr.id) {
446                    attr_state_and_version
447                } else {
448                    continue;
449                };
450
451            if !attr.bannable || attr_state.bans_users.contains(&user_state.id) {
452                continue;
453            }
454
455            let mut attr_state = attr_state.clone();
456            assert!(attr_state.bans_users.insert(user_state.id));
457
458            match app
459                .put_object::<AttrState>(&attr_state, Some(attr_state_version.clone()))
460                .await
461            {
462                Ok(Some(attr_state_version)) => {
463                    assert!(
464                        attr_states
465                            .insert(attr.id, (attr_state.clone(), attr_state_version))
466                            .is_some()
467                    );
468                    assert!(attr_add_status.contains_key(&attr.id));
469                }
470                _ => {
471                    assert!(
472                        attr_add_status
473                            .insert(attr.id, AttrAddStatus::PleaseTryAgain)
474                            .is_none()
475                    );
476                }
477            }
478        }
479
480        let mut new_user_state = user_state.clone();
481        let mut added_attrs: HashSet<Id> = Default::default();
482
483        // Finally check that the attributes are added to the user's account state
484        for (attr_id, (attr_state, ..)) in attr_states {
485            if *attr_add_status.get(&attr_id).unwrap() == AttrAddStatus::PleaseTryAgain {
486                continue;
487            }
488
489            if let Some(identifies_user_id) = attr_state.may_identify_user {
490                assert_eq!(identifies_user_id, user_state.id);
491
492                if new_user_state.allow_login_by.insert(attr_id) {
493                    added_attrs.insert(attr_id);
494                }
495            }
496
497            if attr_state.bans_users.contains(&user_state.id)
498                && new_user_state.could_be_banned_by.insert(attr_id)
499            {
500                added_attrs.insert(attr_id);
501            }
502        }
503
504        if !added_attrs.is_empty() {
505            match app
506                .put_object::<UserState>(&new_user_state, Some(user_state_version))
507                .await
508            {
509                Ok(Some(new_user_state_version)) => {
510                    #[expect(unused_assignments)]
511                    {
512                        user_state_version = new_user_state_version;
513                    }
514
515                    for added_attr_id in added_attrs {
516                        attr_add_status.insert(added_attr_id, AttrAddStatus::Added);
517                    }
518                }
519
520                problem => {
521                    log::warn!("failed to update user state to add attributes: {problem:?}");
522
523                    for added_attr_id in added_attrs {
524                        attr_add_status.insert(added_attr_id, AttrAddStatus::PleaseTryAgain);
525                    }
526                }
527            }
528        }
529        let user_state = new_user_state;
530
531        let auth_token_package = app.issue_auth_token(&user_state)?;
532
533        Ok(EnterResp::Entered {
534            new_account,
535            auth_token_package,
536            attr_status: attr_add_status
537                .iter()
538                .map(|(attr_id, attr_add_status)| {
539                    (attrs.get(attr_id).unwrap().attr.clone(), *attr_add_status)
540                })
541                .collect(),
542        })
543    }
544
545    /// Computes and caches the [`Id`] of an [`Attr`].
546    fn id_attr(&self, attr: Attr) -> IdedAttr {
547        IdedAttr {
548            id: attr.id(&*self.attr_id_secret),
549            attr,
550        }
551    }
552
553    /// Pre-checks whether the given attributes in `attrs` are suitable for
554    /// registering a new user.
555    ///
556    /// Potential problems:
557    ///  1. None of the given attributes is bannable.
558    ///  2. One of the attributes is banned
559    ///  3. One of the attributes already identifies another user.
560    ///  4. One if the attributes already bans another user (if
561    ///     `register_only_with_unique_attrs` is set
562    ///
563    /// Might try to retrieve attribute states for attributes not already in `attr_states`,
564    /// and will add those to `attr_states`. If it did, will set `retrieved_attr_states`.
565    ///
566    /// Returns `Ok(None)` when there are no issues.
567    ///
568    /// The situation can, of course, change between the time of the check and the time of
569    /// registration.
570    async fn precheck_attrs_for_registration(
571        &self,
572        attrs: &HashMap<Id, IdedAttr>,
573        attr_states: &mut HashMap<Id, (AttrState, object_store::UpdateVersion)>,
574        retrieved_attr_states: &mut bool,
575        register_only_with_unique_attrs: bool,
576    ) -> api::Result<Option<EnterResp>> {
577        // Before doing potentially expensive queries to the object store, make sure a bannable
578        // attribute has been provided by the client
579        if !attrs.values().any(|attr| attr.bannable) {
580            return Ok(Some(EnterResp::NoBannableAttribute));
581        }
582
583        assert!(!*retrieved_attr_states, "not expecting double work here");
584
585        // Retrieve attributes states in so far they are available
586        for (attr_id, attr) in attrs {
587            // TODO: parallelize?
588            if attr_states.contains_key(attr_id) {
589                continue;
590            }
591
592            if let Some(attr_state_and_version) = self.get_object::<AttrState>(attr_id).await? {
593                attr_states.insert(attr.id, attr_state_and_version);
594            }
595        }
596
597        *retrieved_attr_states = true;
598
599        for (attr_id, (attr_state, ..)) in attr_states.iter() {
600            if attr_state.banned {
601                return Ok(Some(EnterResp::AttributeBanned(
602                    attrs.get(attr_id).unwrap().attr.clone(),
603                )));
604            }
605
606            if attr_state.may_identify_user.is_some() {
607                return Ok(Some(EnterResp::AttributeAlreadyTaken {
608                    attr: attrs.get(attr_id).unwrap().attr.clone(),
609                    bans_other_user: false,
610                }));
611            }
612
613            if register_only_with_unique_attrs && !attr_state.bans_users.is_empty() {
614                return Ok(Some(EnterResp::AttributeAlreadyTaken {
615                    attr: attrs.get(attr_id).unwrap().attr.clone(),
616                    bans_other_user: true,
617                }));
618            }
619        }
620
621        Ok(None)
622    }
623
624    /// Implements [`RefreshEP`]
625    pub(super) async fn handle_user_refresh(
626        app: Rc<Self>,
627        auth_token: actix_web::web::Header<AuthToken>,
628    ) -> api::Result<RefreshResp> {
629        let Ok((user_state, _)) = app
630            // the `true` means we allow expired access tokens
631            .open_auth_token_and_get_user_state_ext(auth_token.into_inner(), true)
632            .await?
633        else {
634            return Ok(RefreshResp::ReobtainAuthToken);
635        };
636
637        Ok(match app.issue_auth_token(&user_state)? {
638            Ok(atp) => RefreshResp::Success(atp),
639            Err(atdr) => RefreshResp::Denied(atdr),
640        })
641    }
642
643    /// Issues auth token for given user, if allowed
644    fn issue_auth_token(
645        &self,
646        user_state: &UserState,
647    ) -> api::Result<Result<AuthTokenPackage, AuthTokenDeniedReason>> {
648        if user_state.could_be_banned_by.is_empty() {
649            return Ok(Err(AuthTokenDeniedReason::NoBannableAttribute));
650        }
651
652        if user_state.banned {
653            return Ok(Err(AuthTokenDeniedReason::Banned));
654        }
655
656        let iat = jwt::NumericDate::now();
657        let exp = iat + self.auth_token_validity;
658        Ok(Ok(AuthTokenPackage {
659            expires: exp,
660            auth_token: AuthTokenInner {
661                user_id: user_state.id,
662                iat,
663                exp,
664            }
665            .seal(&self.auth_token_secret)?,
666        }))
667    }
668}
669
670/// Plaintext content of [`AuthToken`].
671#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
672pub(super) struct AuthTokenInner {
673    /// The [`Id`] of the user to whom this token has been issued.
674    user_id: Id,
675
676    /// When this token expires.
677    exp: jwt::NumericDate,
678
679    /// When this token was issued.
680    iat: jwt::NumericDate,
681}
682
683impl AuthTokenInner {
684    fn seal(&self, key: &crypto::SealingKey) -> api::Result<AuthToken> {
685        Ok(AuthToken {
686            inner: serde_bytes::ByteBuf::from(crypto::seal(&self, key, b"").map_err(|err| {
687                log::warn!("failed to seal AuthTokenInner: {err}");
688                api::ErrorCode::InternalError
689            })?)
690            .into(),
691        })
692    }
693
694    fn unseal(sealed: &AuthToken, key: &crypto::SealingKey) -> Result<AuthTokenInner, Opaque> {
695        crypto::unseal(&*sealed.inner, key, b"")
696    }
697
698    /// Opens this [`AuthToken`], returning the enclosed user's [`Id`].
699    fn open(self, accept_expired: bool) -> Result<Id, Opaque> {
700        if !accept_expired && self.exp < jwt::NumericDate::now() {
701            return Err(OPAQUE);
702        }
703
704        Ok(self.user_id)
705    }
706}
707
708impl App {
709    /// Opens the given [`AuthToken`] returning the enclosed user's [`Id`].
710    pub(super) fn open_auth_token(&self, auth_token: AuthToken) -> Result<Id, Opaque> {
711        self.open_auth_token_ext(auth_token, false)
712    }
713
714    /// Like [`Self::open_auth_token`], but with the option to accept an expired auth token.
715    pub(super) fn open_auth_token_ext(
716        &self,
717        auth_token: AuthToken,
718        accept_expired: bool,
719    ) -> Result<Id, Opaque> {
720        AuthTokenInner::unseal(&auth_token, &self.auth_token_secret)?.open(accept_expired)
721    }
722
723    /// Opens the given [`AuthToken`] and retrieve the associated [`UserState`].
724    ///
725    /// Returns `Ok(Err(Opaque))` when the auth token was invalid.
726    pub(super) async fn open_auth_token_and_get_user_state(
727        &self,
728        auth_token: AuthToken,
729    ) -> api::Result<Result<(UserState, object_store::UpdateVersion), Opaque>> {
730        self.open_auth_token_and_get_user_state_ext(auth_token, false)
731            .await
732    }
733
734    /// Like [`Self::open_auth_token_and_get_user_state`] but with the option to accept an expired auth
735    /// token.
736    pub(super) async fn open_auth_token_and_get_user_state_ext(
737        &self,
738        auth_token: AuthToken,
739        accept_expired: bool,
740    ) -> api::Result<Result<(UserState, object_store::UpdateVersion), Opaque>> {
741        let Ok(user_id) = self.open_auth_token_ext(auth_token, accept_expired) else {
742            return Ok(Err(OPAQUE));
743        };
744
745        Ok(Ok(self
746            .get_object::<UserState>(&user_id)
747            .await?
748            .ok_or_else(|| {
749                log::error!(
750                    "auth token refers to non- (or no longer) existing user with id {user_id}",
751                );
752                api::ErrorCode::InternalError
753            })?))
754    }
755}
756
757/// An [`Attr`] with its [`Id`].
758#[derive(Clone)]
759struct IdedAttr {
760    id: Id,
761    attr: Attr,
762}
763
764impl IdedAttr {
765    #[expect(dead_code)]
766    pub fn id(&self) -> Id {
767        unimplemented!("use the field `id` instead")
768    }
769}
770
771impl Deref for IdedAttr {
772    type Target = Attr;
773
774    fn deref(&self) -> &Attr {
775        &self.attr
776    }
777}
778
779/// Details pubhubs central stores about a user's account
780#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
781pub struct UserState {
782    /// Randomly generated identifier for this account, used for creating access tokens and such
783    pub id: Id,
784
785    /// Used as registration pseudonym on pubhubs cards issued for to this user.
786    ///
787    /// Might not be set for users that registered under v3.0.0, but will be upon entering pubhubs.
788    ///
789    /// If not set, it's derived from [`UserState::id`], which is done by [`UserState::card_id()`].
790    #[serde(default)]
791    card_id: Option<CardPseud>,
792
793    /// Registration date for this user
794    ///
795    /// Might not be set for users that registered under v3.0.0.
796    #[serde(default)]
797    pub registration_date: Option<api::NumericDate>,
798
799    /// Randomly generated and by [`Constellation::master_enc_key`] elgamal encrypted
800    /// identifier used to generate hub pseudonyms for this user.
801    ///
802    /// [`Constellation::master_enc_key`]: crate::servers::constellation::Inner::master_enc_key
803    pub polymorphic_pseudonym: elgamal::Triple,
804
805    /// Whether this account is banned
806    pub banned: bool,
807
808    // TODO: limit number of allow_login_by attributes
809    /// Attributes that may be used to log in as this user,
810    /// provided that [`AttrState::may_identify_user`] also points to this account.
811    ///
812    /// The user may remove an attribute from this list.
813    pub allow_login_by: HashSet<Id>,
814
815    /// Attributes that when banned will ban this user
816    ///
817    /// The user can only add attributes to this list, but not remove them.
818    ///
819    /// This list is used to keep track of whether there is at least one attribute that would
820    /// ban this user.  If there are none, the user must add a bannable attribute before they can
821    /// login in.
822    pub could_be_banned_by: HashSet<Id>,
823
824    /// Details about the objects stored by this user at pubhubs central
825    pub stored_objects: HashMap<handle::Handle, super::user_object_store::UserObjectDetails>,
826}
827
828impl UserState {
829    /// Returns [`UserState::card_id`] when available, and otherwise an [`Id`] derived from [`UserState::id`].
830    pub fn card_id(&self) -> CardPseud {
831        if let Some(card_id) = self.card_id {
832            return card_id;
833        }
834
835        CardPseud(b"".as_slice().derive_id(
836            sha2::Sha256::new().chain_update(self.id.as_slice()),
837            "pubhubs-card-id",
838        ))
839    }
840
841    /// Subtract quota usage from the given [`Quota`], returning an error when a [`QuotumName`] was
842    /// reached.
843    pub(crate) fn update_quota(&self, mut quota: Quota) -> Result<Quota, QuotumName> {
844        quota.object_count = quota
845            .object_count
846            .checked_sub(self.stored_objects.len().try_into().unwrap_or(u16::MAX))
847            .ok_or_else(|| {
848                let quotum = QuotumName::ObjectCount;
849                log::warn!("user {} has reached quotum {quotum}", self.id);
850                quotum
851            })?;
852
853        for sod in self.stored_objects.values() {
854            quota.object_bytes_total =
855                quota
856                    .object_bytes_total
857                    .checked_sub(sod.size)
858                    .ok_or_else(|| {
859                        let quotum = QuotumName::ObjectBytesTotal;
860                        log::warn!("user {} has reached quotum {quotum}", self.id);
861                        quotum
862                    })?;
863        }
864
865        Ok(quota)
866    }
867
868    /// Turns this [`UserState`] into a [`ApiUserState`].
869    pub(crate) fn into_user_version(self: UserState, app: &App) -> ApiUserState {
870        ApiUserState {
871            allow_login_by: self.allow_login_by,
872            could_be_banned_by: self.could_be_banned_by,
873            stored_objects: self
874                .stored_objects
875                .into_iter()
876                .map(|(handle, uod)| (handle, uod.into_user_version(&app.user_object_hmac_secret)))
877                .collect(),
878        }
879    }
880}