1use std::collections::HashMap;
4
5use anyhow::anyhow;
6use lexe_crypto::ed25519;
7use lexe_serde::{
8 base64_or_bytes,
9 optopt::{self, none},
10};
11#[cfg(any(test, feature = "test-utils"))]
12use proptest_derive::Arbitrary;
13use serde::{Deserialize, Serialize};
14
15use crate::{
16 api::{auth::Scope, user::UserPk},
17 time::TimestampMs,
18};
19
20#[derive(Clone, Debug, Default, Serialize, Deserialize)]
26pub struct RevocableClients {
27 pub clients: HashMap<ed25519::PublicKey, RevocableClient>,
28}
29
30impl RevocableClients {
31 pub const MAX_LEN: usize = 100;
33
34 pub fn iter_valid(
36 &self,
37 ) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
38 self.iter_valid_at(TimestampMs::now())
39 }
40
41 pub fn iter_valid_at(
43 &self,
44 now: TimestampMs,
45 ) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
46 self.clients
47 .iter()
48 .filter(|(_k, v)| !v.is_revoked)
49 .filter(move |(_k, v)| !v.is_expired_at(now))
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
57pub struct RevocableClient {
58 pub pubkey: ed25519::PublicKey,
61 pub created_at: TimestampMs,
63 pub expires_at: Option<TimestampMs>,
67 #[cfg_attr(
69 any(test, feature = "test-utils"),
70 proptest(strategy = "arb::any_label()")
71 )]
72 pub label: Option<String>,
73 pub scope: Scope,
76 pub is_revoked: bool,
78 }
80
81impl RevocableClient {
82 pub const MAX_LABEL_LEN: usize = 64;
84
85 #[must_use]
87 pub fn is_valid_at(&self, now: TimestampMs) -> bool {
88 !self.is_revoked && !self.is_expired_at(now)
89 }
90
91 #[must_use]
93 pub fn is_expired_at(&self, now: TimestampMs) -> bool {
94 if let Some(expiration) = self.expires_at
95 && now > expiration
96 {
97 return true;
98 }
99
100 false
101 }
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize)]
106#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq, Arbitrary))]
107pub struct GetRevocableClients {
108 pub valid_only: bool,
110}
111
112#[derive(Clone, Debug, Serialize, Deserialize)]
114pub struct CreateRevocableClientRequest {
115 pub expires_at: Option<TimestampMs>,
118 pub label: Option<String>,
120 pub scope: Scope,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct CreateRevocableClientResponse {
127 #[serde(skip_serializing_if = "Option::is_none")]
130 pub user_pk: Option<UserPk>,
131
132 pub pubkey: ed25519::PublicKey,
134 pub created_at: TimestampMs,
136
137 #[serde(with = "base64_or_bytes")]
142 pub eph_ca_cert_der: Vec<u8>,
143
144 #[serde(with = "base64_or_bytes")]
147 pub rev_client_cert_der: Vec<u8>,
148
149 #[serde(with = "base64_or_bytes")]
151 pub rev_client_cert_key_der: Vec<u8>,
152}
153
154#[derive(Serialize, Deserialize)]
163#[cfg_attr(test, derive(Debug, Eq, PartialEq, Arbitrary))]
164pub struct UpdateClientRequest {
165 pub pubkey: ed25519::PublicKey,
167
168 #[serde(default, skip_serializing_if = "none", with = "optopt")]
170 pub expires_at: Option<Option<TimestampMs>>,
171
172 #[serde(default, skip_serializing_if = "none", with = "optopt")]
174 #[cfg_attr(test, proptest(strategy = "arb::any_label_update()"))]
175 pub label: Option<Option<String>>,
176
177 #[serde(skip_serializing_if = "none")]
179 pub scope: Option<Scope>,
180
181 #[serde(skip_serializing_if = "none")]
184 pub is_revoked: Option<bool>,
185}
186
187#[derive(Serialize, Deserialize)]
189pub struct UpdateClientResponse {
190 pub client: RevocableClient,
191}
192
193impl RevocableClient {
194 pub fn update(&self, req: UpdateClientRequest) -> anyhow::Result<Self> {
196 let UpdateClientRequest {
197 pubkey: req_pubkey,
198 expires_at: req_expires_at,
199 label: req_label,
200 scope: req_scope,
201 is_revoked: req_is_revoked,
202 } = req;
203
204 let mut out = self.clone();
205
206 if self.pubkey != req_pubkey {
207 debug_assert!(false);
208 return Err(anyhow!("Cannot update a different client"));
209 }
210
211 if let Some(expires_at) = req_expires_at {
212 out.expires_at = expires_at;
214 }
215
216 if let Some(maybe_label) = req_label {
217 if let Some(label) = &maybe_label
218 && label.len() > Self::MAX_LABEL_LEN
219 {
220 return Err(anyhow!(
221 "Label must not be longer than {} bytes",
222 Self::MAX_LABEL_LEN,
223 ));
224 }
225 out.label = maybe_label;
226 }
227
228 if let Some(scope) = req_scope {
229 out.scope = scope;
232 }
233
234 if let Some(revoke) = req_is_revoked {
235 if self.is_revoked && !revoke {
236 return Err(anyhow!("Cannot unrevoke a client"));
237 }
238 out.is_revoked = revoke;
239 }
240
241 Ok(out)
242 }
243}
244
245#[cfg(any(test, feature = "test-utils"))]
246mod arb {
247 use std::ops::RangeInclusive;
248
249 use proptest::{collection::vec, option, strategy::Strategy};
250
251 use super::*;
252 use crate::test_utils::arbitrary;
253
254 pub fn any_label() -> impl Strategy<Value = Option<String>> {
255 static RANGES: &[RangeInclusive<char>] =
256 &['0'..='9', 'A'..='Z', 'a'..='z'];
257 let any_alphanum_char = proptest::char::ranges(RANGES.into());
258 option::of(
259 vec(any_alphanum_char, 0..=RevocableClient::MAX_LABEL_LEN)
260 .prop_map(String::from_iter),
261 )
262 }
263
264 #[allow(dead_code)]
265 pub fn any_label_update() -> impl Strategy<Value = Option<Option<String>>> {
266 option::of(arbitrary::any_option_simple_string())
267 }
268}
269
270#[cfg(test)]
271mod test {
272 use super::*;
273 use crate::{root_seed::RootSeed, test_utils::roundtrip};
274
275 #[test]
276 fn rev_client_ser_basic() {
277 let client1 = RevocableClient {
278 pubkey: *RootSeed::from_u64(1).derive_user_key_pair().public_key(),
279 created_at: TimestampMs::from_secs_u32(69),
280 expires_at: Some(TimestampMs::from_secs_u32(420)),
281 label: Some("deez".to_string()),
282 scope: Scope::All,
283 is_revoked: false,
284 };
285 let client_json = serde_json::to_string_pretty(&client1).unwrap();
286 let client_json_snapshot = r#"{
288 "pubkey": "aa8e3e1a9bffdb073507f23474100619fdd4e392ef0ff1e89348252f287a06fc",
289 "created_at": 69000,
290 "expires_at": 420000,
291 "label": "deez",
292 "scope": "All",
293 "is_revoked": false
294}"#;
295 assert_eq!(client_json, client_json_snapshot);
296
297 let client2 =
298 serde_json::from_str::<RevocableClient>(&client_json).unwrap();
299 assert_eq!(client1, client2);
300 }
301
302 #[test]
303 fn test_update_request_serde() {
304 roundtrip::json_string_roundtrip_proptest::<UpdateClientRequest>();
305 }
306
307 #[test]
308 fn test_get_revocable_clients_serde() {
309 roundtrip::query_string_roundtrip_proptest::<GetRevocableClients>();
310 }
311}