Skip to main content

pubhubs/
attr.rs

1//! Attributes, for identifying (and/or banning) end-users
2
3use std::collections::HashSet;
4
5use crate::common::secret;
6use crate::handle::{Handle, Handles};
7use crate::id::Id;
8use crate::phcrypto;
9use crate::servers::yivi;
10
11#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
12pub struct Type {
13    /// Immutable
14    pub id: Id,
15
16    /// For referring to this attribute type from code - only add handles; don't remove them
17    pub handles: Handles,
18
19    /// Whether [`Attr`]ibutes of this type can be used to ban users.  Users must provide such a
20    /// bannable attribute.
21    pub bannable: bool,
22
23    /// Whether [`Attr`]ibutes of this type can be used to identify a users.
24    pub identifying: bool,
25
26    /// The different 'regular' ways this attribute can be obtained via [`crate::api::auths::AuthStartEP`].
27    ///
28    /// Some attributes, like the pubhubs card attribute, can also be obtained in irregular ways.
29    pub sources: Vec<SourceDetails>,
30
31    #[serde(default)]
32    /// If set, an instance of this attribute obtained by [`crate::api::auths::AuthStartEP`] can not be
33    /// added to a user account.  Pubhubs card attribute types have this flag set - to get an
34    /// pubhubs card attribute instance that is addable, use [`crate::api::auths::CardEP`].
35    pub not_addable_by_default: bool,
36}
37
38impl std::fmt::Display for Type {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "{}", self.handles.preferred())
41    }
42}
43
44impl Type {
45    /// Iterates over all the [`yivi::AttributeTypeIdentifier`]s that can be used to obtain this
46    /// attribite type.
47    pub fn yivi_attr_type_ids(&self) -> impl Iterator<Item = &yivi::AttributeTypeIdentifier> {
48        #[expect(clippy::unnecessary_filter_map)] // remove when we get more sources
49        self.sources.iter().filter_map(|source| match source {
50            SourceDetails::Yivi { attr_type_id } => Some(attr_type_id),
51        })
52    }
53
54    /// Removes the [`SourceDetails`] from [`Source`]s not accepted by `accept_source`.
55    pub(crate) fn filter_sources(&mut self, accept_source: impl Fn(Source) -> bool) {
56        self.sources
57            .retain(|source_details| accept_source(source_details.source()))
58    }
59}
60
61/// Instructions on how to obtain an [`Attr`]ibute of a particular [`Type`].
62#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
63#[serde(rename_all = "snake_case")]
64pub enum SourceDetails {
65    Yivi {
66        /// The yivi attribute type identifier
67        attr_type_id: yivi::AttributeTypeIdentifier,
68    },
69}
70
71impl SourceDetails {
72    pub fn source(&self) -> Source {
73        match &self {
74            SourceDetails::Yivi { .. } => Source::Yivi,
75        }
76    }
77}
78
79#[derive(serde::Deserialize, serde::Serialize, Hash, Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Source {
81    Yivi,
82}
83
84impl crate::map::Handled for Type {
85    fn handles(&self) -> &[Handle] {
86        &self.handles
87    }
88
89    fn id(&self) -> &Id {
90        &self.id
91    }
92}
93
94#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
95pub struct Attr {
96    /// Refers to the this attribute's [`Type`] via the type's [`Id`].
97    pub attr_type: Id,
98
99    /// Actual value of this attribute, in a format that is [`Type`] dependent.
100    pub value: String,
101
102    #[serde(skip_serializing_if = "std::ops::Not::not")]
103    #[serde(default)]
104    pub bannable: bool,
105
106    /// Whether the attribute is not identifying.  We use the negation so that the default will be
107    /// that the attribute _is_ identifying, which most attributes are.
108    #[serde(skip_serializing_if = "std::ops::Not::not")]
109    #[serde(default)]
110    pub not_identifying: bool,
111
112    /// Whether this particular attribute can _not_ be added to a user account.
113    ///
114    /// A pubhubs card attribute obtained via Yivi disclosure is not addable, for example.
115    #[serde(skip_serializing_if = "std::ops::Not::not")]
116    #[serde(default)]
117    pub not_addable: bool,
118}
119
120impl Attr {
121    /// Derives an identifier for this attribute from [`Attr::value`] and [`Attr::attr_type`],
122    /// and the given digestible secret.
123    pub fn id(&self, secret: impl secret::DigestibleSecret) -> Id {
124        phcrypto::attr_id(self, secret)
125    }
126}
127
128// So Signed<Attr> can be used.
129crate::api::having_message_code! {Attr, Attr}
130
131/// State of an [`Attr`] according to pubhubs central.
132#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
133pub struct AttrState {
134    pub attr: Id,
135
136    /// Whether this attribute has been banned.
137    #[serde(default)]
138    pub banned: bool,
139
140    /// The user, if any, that this attribute can identify.
141    ///
142    /// Only identifies the user if the user lists this attribute among its identifying attributes.
143    ///
144    /// Once set, this should never be unset.  This prevents impersonation of a user when
145    /// they remove their id.
146    #[serde(default)]
147    pub may_identify_user: Option<Id>,
148
149    // TODO: limit size of this set
150    /// The users that provided this attribute as bannable attribute.
151    /// If this attribute gets banned, so will they.
152    #[serde(default)]
153    pub bans_users: HashSet<Id>,
154}
155
156impl AttrState {
157    pub fn new(attr_id: Id, attr: &Attr, user_id: Id) -> Self {
158        Self {
159            attr: attr_id,
160            banned: false,
161            may_identify_user: if attr.not_identifying {
162                None
163            } else {
164                Some(user_id)
165            },
166            bans_users: if attr.bannable {
167                std::iter::once(user_id).collect()
168            } else {
169                Default::default()
170            },
171        }
172    }
173}