lexe_api_core/
revocable_clients.rs1use std::{collections::HashMap, sync::RwLock};
4
5use anyhow::anyhow;
6use lexe_common::{
7 api::{
8 auth::LexeScope,
9 revocable_clients::{GetRevocableClientStatus, RevocableClientStatus},
10 },
11 time::TimestampMs,
12};
13use lexe_crypto::ed25519;
14#[cfg(any(test, feature = "test-utils"))]
15use proptest_derive::Arbitrary;
16use serde::{Deserialize, Serialize};
17
18use self::models::UpdateClientRequest;
19
20pub mod models;
22
23#[derive(Debug)]
26pub struct RevocableClientsHandle(pub RwLock<RevocableClients>);
27
28impl GetRevocableClientStatus for RevocableClientsHandle {
29 fn get_client_status(
30 &self,
31 client_pk: &ed25519::PublicKey,
32 now: TimestampMs,
33 ) -> Option<RevocableClientStatus> {
34 let clients = self.0.read().unwrap();
35 let client = clients.clients.get(client_pk)?;
36 let status = if client.is_revoked {
37 RevocableClientStatus::Revoked
38 } else if client.is_expired_at(now) {
39 RevocableClientStatus::Expired
40 } else {
41 RevocableClientStatus::Valid
42 };
43 Some(status)
44 }
45}
46
47#[derive(Clone, Debug, Default, Serialize, Deserialize)]
53pub struct RevocableClients {
54 pub clients: HashMap<ed25519::PublicKey, RevocableClient>,
55}
56
57impl RevocableClients {
58 pub const MAX_LEN: usize = 100;
60
61 pub fn iter_valid(
63 &self,
64 ) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
65 self.iter_valid_at(TimestampMs::now())
66 }
67
68 pub fn iter_valid_at(
70 &self,
71 now: TimestampMs,
72 ) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
73 self.clients
74 .iter()
75 .filter(|(_k, v)| !v.is_revoked)
76 .filter(move |(_k, v)| !v.is_expired_at(now))
77 }
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
84pub struct RevocableClient {
85 pub pubkey: ed25519::PublicKey,
88 pub created_at: TimestampMs,
90 pub expires_at: Option<TimestampMs>,
94 #[cfg_attr(
96 any(test, feature = "test-utils"),
97 proptest(strategy = "arb::any_label()")
98 )]
99 pub label: Option<String>,
100 pub scope: LexeScope,
103 pub is_revoked: bool,
105 }
107
108impl RevocableClient {
109 pub const MAX_LABEL_LEN: usize = 64;
111
112 #[must_use]
114 pub fn is_valid_at(&self, now: TimestampMs) -> bool {
115 !self.is_revoked && !self.is_expired_at(now)
116 }
117
118 #[must_use]
120 pub fn is_expired_at(&self, now: TimestampMs) -> bool {
121 if let Some(expiration) = self.expires_at
122 && now > expiration
123 {
124 return true;
125 }
126
127 false
128 }
129
130 pub fn update(&self, req: UpdateClientRequest) -> anyhow::Result<Self> {
132 let UpdateClientRequest {
133 pubkey: req_pubkey,
134 expires_at: req_expires_at,
135 label: req_label,
136 scope: req_scope,
137 is_revoked: req_is_revoked,
138 } = req;
139
140 let mut out = self.clone();
141
142 if self.pubkey != req_pubkey {
143 debug_assert!(false);
144 return Err(anyhow!("Cannot update a different client"));
145 }
146
147 if let Some(expires_at) = req_expires_at {
148 out.expires_at = expires_at;
150 }
151
152 if let Some(maybe_label) = req_label {
153 if let Some(label) = &maybe_label
154 && label.len() > Self::MAX_LABEL_LEN
155 {
156 return Err(anyhow!(
157 "Label must not be longer than {} bytes",
158 Self::MAX_LABEL_LEN,
159 ));
160 }
161 out.label = maybe_label;
162 }
163
164 if let Some(scope) = req_scope {
165 out.scope = scope;
168 }
169
170 if let Some(revoke) = req_is_revoked {
171 if self.is_revoked && !revoke {
172 return Err(anyhow!("Cannot unrevoke a client"));
173 }
174 out.is_revoked = revoke;
175 }
176
177 Ok(out)
178 }
179}
180
181#[cfg(any(test, feature = "test-utils"))]
182mod arb {
183 use std::ops::RangeInclusive;
184
185 use proptest::{collection::vec, option, strategy::Strategy};
186
187 use super::*;
188
189 pub fn any_label() -> impl Strategy<Value = Option<String>> {
190 static RANGES: &[RangeInclusive<char>] =
191 &['0'..='9', 'A'..='Z', 'a'..='z'];
192 let any_alphanum_char = proptest::char::ranges(RANGES.into());
193 option::of(
194 vec(any_alphanum_char, 0..=RevocableClient::MAX_LABEL_LEN)
195 .prop_map(String::from_iter),
196 )
197 }
198}
199
200#[cfg(test)]
201mod test {
202 use lexe_common::root_seed::RootSeed;
203
204 use super::*;
205
206 #[test]
207 fn rev_client_ser_basic() {
208 let client1 = RevocableClient {
209 pubkey: *RootSeed::from_u64(1).derive_user_key_pair().public_key(),
210 created_at: TimestampMs::from_secs_u32(69),
211 expires_at: Some(TimestampMs::from_secs_u32(420)),
212 label: Some("deez".to_string()),
213 scope: LexeScope::All,
214 is_revoked: false,
215 };
216 let client_json = serde_json::to_string_pretty(&client1).unwrap();
217 let client_json_snapshot = r#"{
219 "pubkey": "aa8e3e1a9bffdb073507f23474100619fdd4e392ef0ff1e89348252f287a06fc",
220 "created_at": 69000,
221 "expires_at": 420000,
222 "label": "deez",
223 "scope": "All",
224 "is_revoked": false
225}"#;
226 assert_eq!(client_json, client_json_snapshot);
227
228 let client2 =
229 serde_json::from_str::<RevocableClient>(&client_json).unwrap();
230 assert_eq!(client1, client2);
231 }
232}