kanidm_proto/internal/
token.rs

1use super::UiHint;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4use std::fmt;
5use time::OffsetDateTime;
6use utoipa::ToSchema;
7use uuid::Uuid;
8
9use serde_with::skip_serializing_none;
10
11#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
12#[serde(rename_all = "lowercase")]
13pub enum UatPurpose {
14    ReadOnly,
15    ReadWrite {
16        /// If none, there is no expiry, and this is always rw. If there is
17        /// an expiry, check that the current time < expiry.
18        #[serde(with = "time::serde::timestamp::option")]
19        expiry: Option<time::OffsetDateTime>,
20    },
21}
22
23/// The currently authenticated user, and any required metadata for them
24/// to properly authorise them. This is similar in nature to oauth and the krb
25/// PAC/PAD structures. This information is transparent to clients and CAN
26/// be parsed by them!
27///
28/// This structure and how it works will *very much* change over time from this
29/// point onward! This means on updates, that sessions will invalidate in many
30/// cases.
31#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
32#[skip_serializing_none]
33#[serde(rename_all = "lowercase")]
34pub struct UserAuthToken {
35    pub session_id: Uuid,
36    #[serde(with = "time::serde::timestamp")]
37    pub issued_at: time::OffsetDateTime,
38    /// If none, there is no expiry, and this is always valid. If there is
39    /// an expiry, check that the current time < expiry.
40    #[serde(with = "time::serde::timestamp::option")]
41    pub expiry: Option<time::OffsetDateTime>,
42    pub purpose: UatPurpose,
43    pub uuid: Uuid,
44    pub displayname: String,
45    pub spn: String,
46    pub mail_primary: Option<String>,
47    pub ui_hints: BTreeSet<UiHint>,
48
49    pub limit_search_max_results: Option<u64>,
50    pub limit_search_max_filter_test: Option<u64>,
51}
52
53impl fmt::Display for UserAuthToken {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        writeln!(f, "spn: {}", self.spn)?;
56        writeln!(f, "uuid: {}", self.uuid)?;
57        writeln!(f, "display: {}", self.displayname)?;
58        if let Some(exp) = self.expiry {
59            writeln!(f, "expiry: {exp}")?;
60        } else {
61            writeln!(f, "expiry: -")?;
62        }
63        match &self.purpose {
64            UatPurpose::ReadOnly => writeln!(f, "purpose: read only")?,
65            UatPurpose::ReadWrite {
66                expiry: Some(expiry),
67            } => writeln!(f, "purpose: read write (expiry: {expiry})")?,
68            UatPurpose::ReadWrite { expiry: None } => {
69                writeln!(f, "purpose: read write (expiry: none)")?
70            }
71        }
72        Ok(())
73    }
74}
75
76impl PartialEq for UserAuthToken {
77    fn eq(&self, other: &Self) -> bool {
78        self.session_id == other.session_id
79    }
80}
81
82impl Eq for UserAuthToken {}
83
84pub enum PrivilegesActive {
85    /// This session has active read write privs.
86    True,
87    /// This session can become read-write, but requires reauth to proceed.
88    ReauthRequired,
89    /// This session has no privileges and is read only
90    False,
91}
92
93impl UserAuthToken {
94    pub fn name(&self) -> &str {
95        self.spn.split_once('@').map(|x| x.0).unwrap_or(&self.spn)
96    }
97
98    /// Show if the uat at a current point in time has active read-write
99    /// capabilities.
100    pub fn purpose_privilege_state(&self, ct: time::OffsetDateTime) -> PrivilegesActive {
101        match self.purpose {
102            UatPurpose::ReadWrite { expiry: Some(exp) } if ct < exp => PrivilegesActive::True,
103            // The privileges have expired, or are not yet activated on this session.
104            UatPurpose::ReadWrite { expiry: Some(_) } | UatPurpose::ReadWrite { expiry: None } => {
105                PrivilegesActive::ReauthRequired
106            }
107            UatPurpose::ReadOnly => PrivilegesActive::False,
108        }
109    }
110}
111
112#[derive(Debug, Serialize, Deserialize, Clone, Default, ToSchema)]
113#[serde(rename_all = "lowercase")]
114pub enum ApiTokenPurpose {
115    #[default]
116    ReadOnly,
117    ReadWrite,
118    Synchronise,
119}
120
121#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
122#[serde(rename_all = "lowercase")]
123pub struct ApiToken {
124    // The account this is associated with.
125    pub account_id: Uuid,
126    pub token_id: Uuid,
127    pub label: String,
128    #[serde(with = "time::serde::timestamp::option")]
129    pub expiry: Option<time::OffsetDateTime>,
130    #[serde(with = "time::serde::timestamp")]
131    pub issued_at: time::OffsetDateTime,
132    // Defaults to ReadOnly if not present
133    #[serde(default)]
134    pub purpose: ApiTokenPurpose,
135}
136
137impl fmt::Display for ApiToken {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        writeln!(f, "account_id: {}", self.account_id)?;
140        writeln!(f, "token_id: {}", self.token_id)?;
141        writeln!(f, "label: {}", self.label)?;
142        writeln!(f, "issued at: {}", self.issued_at)?;
143        if let Some(expiry) = self.expiry {
144            // if this fails we're in trouble!
145            #[allow(clippy::expect_used)]
146            let expiry_str = expiry
147                .to_offset(
148                    time::UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH)
149                        .unwrap_or(time::UtcOffset::UTC),
150                )
151                .format(&time::format_description::well_known::Rfc3339)
152                .expect("Failed to format timestamp to RFC3339");
153            writeln!(f, "token expiry: {expiry_str}")
154        } else {
155            writeln!(f, "token expiry: never")
156        }
157    }
158}
159
160impl PartialEq for ApiToken {
161    fn eq(&self, other: &Self) -> bool {
162        self.token_id == other.token_id
163    }
164}
165
166impl Eq for ApiToken {}
167
168// This is similar to uat, but omits claims (they have no role in radius), and adds
169// the radius secret field.
170#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
171pub struct RadiusAuthToken {
172    pub name: String,
173    pub displayname: String,
174    pub uuid: String,
175    pub secret: String,
176    pub groups: Vec<Group>,
177}
178
179impl fmt::Display for RadiusAuthToken {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        writeln!(f, "name: {}", self.name)?;
182        writeln!(f, "displayname: {}", self.displayname)?;
183        writeln!(f, "uuid: {}", self.uuid)?;
184        writeln!(f, "secret: {}", self.secret)?;
185        self.groups
186            .iter()
187            .try_for_each(|g| writeln!(f, "group: {g}"))
188    }
189}
190
191#[derive(Debug, Serialize, Deserialize, Clone)]
192#[serde(rename_all = "lowercase")]
193pub struct ScimSyncToken {
194    // uuid of the token?
195    pub token_id: Uuid,
196    #[serde(with = "time::serde::timestamp")]
197    pub issued_at: time::OffsetDateTime,
198    #[serde(default)]
199    pub purpose: ApiTokenPurpose,
200}
201
202#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
203pub struct Group {
204    pub spn: String,
205    pub uuid: String,
206}
207
208impl fmt::Display for Group {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        write!(f, "[ spn: {}, ", self.spn)?;
211        write!(f, "uuid: {} ]", self.uuid)
212    }
213}