use std::{collections::HashMap, sync::RwLock};
use anyhow::anyhow;
use lexe_common::{
api::{
auth::LexeScope,
revocable_clients::{GetRevocableClientStatus, RevocableClientStatus},
},
time::TimestampMs,
};
use lexe_crypto::ed25519;
#[cfg(any(test, feature = "test-utils"))]
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
use self::models::UpdateClientRequest;
pub mod models;
#[derive(Debug)]
pub struct RevocableClientsHandle(pub RwLock<RevocableClients>);
impl GetRevocableClientStatus for RevocableClientsHandle {
fn get_client_status(
&self,
client_pk: &ed25519::PublicKey,
now: TimestampMs,
) -> Option<RevocableClientStatus> {
let clients = self.0.read().unwrap();
let client = clients.clients.get(client_pk)?;
let status = if client.is_revoked {
RevocableClientStatus::Revoked
} else if client.is_expired_at(now) {
RevocableClientStatus::Expired
} else {
RevocableClientStatus::Valid
};
Some(status)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RevocableClients {
pub clients: HashMap<ed25519::PublicKey, RevocableClient>,
}
impl RevocableClients {
pub const MAX_LEN: usize = 100;
pub fn iter_valid(
&self,
) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
self.iter_valid_at(TimestampMs::now())
}
pub fn iter_valid_at(
&self,
now: TimestampMs,
) -> impl Iterator<Item = (&ed25519::PublicKey, &RevocableClient)> {
self.clients
.iter()
.filter(|(_k, v)| !v.is_revoked)
.filter(move |(_k, v)| !v.is_expired_at(now))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
pub struct RevocableClient {
pub pubkey: ed25519::PublicKey,
pub created_at: TimestampMs,
pub expires_at: Option<TimestampMs>,
#[cfg_attr(
any(test, feature = "test-utils"),
proptest(strategy = "arb::any_label()")
)]
pub label: Option<String>,
pub scope: LexeScope,
pub is_revoked: bool,
}
impl RevocableClient {
pub const MAX_LABEL_LEN: usize = 64;
#[must_use]
pub fn is_valid_at(&self, now: TimestampMs) -> bool {
!self.is_revoked && !self.is_expired_at(now)
}
#[must_use]
pub fn is_expired_at(&self, now: TimestampMs) -> bool {
if let Some(expiration) = self.expires_at
&& now > expiration
{
return true;
}
false
}
pub fn update(&self, req: UpdateClientRequest) -> anyhow::Result<Self> {
let UpdateClientRequest {
pubkey: req_pubkey,
expires_at: req_expires_at,
label: req_label,
scope: req_scope,
is_revoked: req_is_revoked,
} = req;
let mut out = self.clone();
if self.pubkey != req_pubkey {
debug_assert!(false);
return Err(anyhow!("Cannot update a different client"));
}
if let Some(expires_at) = req_expires_at {
out.expires_at = expires_at;
}
if let Some(maybe_label) = req_label {
if let Some(label) = &maybe_label
&& label.len() > Self::MAX_LABEL_LEN
{
return Err(anyhow!(
"Label must not be longer than {} bytes",
Self::MAX_LABEL_LEN,
));
}
out.label = maybe_label;
}
if let Some(scope) = req_scope {
out.scope = scope;
}
if let Some(revoke) = req_is_revoked {
if self.is_revoked && !revoke {
return Err(anyhow!("Cannot unrevoke a client"));
}
out.is_revoked = revoke;
}
Ok(out)
}
}
#[cfg(any(test, feature = "test-utils"))]
mod arb {
use std::ops::RangeInclusive;
use proptest::{collection::vec, option, strategy::Strategy};
use super::*;
pub fn any_label() -> impl Strategy<Value = Option<String>> {
static RANGES: &[RangeInclusive<char>] =
&['0'..='9', 'A'..='Z', 'a'..='z'];
let any_alphanum_char = proptest::char::ranges(RANGES.into());
option::of(
vec(any_alphanum_char, 0..=RevocableClient::MAX_LABEL_LEN)
.prop_map(String::from_iter),
)
}
}
#[cfg(test)]
mod test {
use lexe_common::root_seed::RootSeed;
use super::*;
#[test]
fn rev_client_ser_basic() {
let client1 = RevocableClient {
pubkey: *RootSeed::from_u64(1).derive_user_key_pair().public_key(),
created_at: TimestampMs::from_secs_u32(69),
expires_at: Some(TimestampMs::from_secs_u32(420)),
label: Some("deez".to_string()),
scope: LexeScope::All,
is_revoked: false,
};
let client_json = serde_json::to_string_pretty(&client1).unwrap();
let client_json_snapshot = r#"{
"pubkey": "aa8e3e1a9bffdb073507f23474100619fdd4e392ef0ff1e89348252f287a06fc",
"created_at": 69000,
"expires_at": 420000,
"label": "deez",
"scope": "All",
"is_revoked": false
}"#;
assert_eq!(client_json, client_json_snapshot);
let client2 =
serde_json::from_str::<RevocableClient>(&client_json).unwrap();
assert_eq!(client1, client2);
}
}