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