lfsx_server/console/
tokens.rs1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use crate::error::Error;
7use crate::storage::Store;
8
9const FILE: &str = "dashboard-tokens.json";
10const PREFIX: &str = "lfsx_";
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Issued {
14 pub name: String,
15 pub created: u64,
16 hash: String,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum Refusal {
21 #[error("a token name is 1 to 64 letters, digits, dots, dashes or underscores")]
22 Unreadable,
23 #[error("a token named {0} already exists")]
24 Taken(String),
25 #[error("no token is named {0}")]
26 Unknown(String),
27 #[error(transparent)]
28 Store(#[from] Error),
29}
30
31fn digest(token: &str) -> String {
32 hex::encode(Sha256::digest(token.as_bytes()))
33}
34
35fn readable(name: &str) -> bool {
36 (1..=64).contains(&name.len())
37 && name
38 .chars()
39 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
40}
41
42pub async fn issued(store: &Store) -> Result<Vec<Issued>, Error> {
43 match store.read_meta(FILE).await? {
44 Some(bytes) => Ok(serde_json::from_slice(&bytes)?),
45 None => Ok(Vec::new()),
46 }
47}
48
49async fn keep(store: &Store, issued: &[Issued]) -> Result<(), Error> {
50 store
51 .write_meta(FILE, serde_json::to_vec_pretty(issued)?)
52 .await
53}
54
55pub async fn create(store: &Store, name: &str) -> Result<String, Refusal> {
56 if !readable(name) {
57 return Err(Refusal::Unreadable);
58 }
59 let mut issued = issued(store).await?;
60 if issued.iter().any(|token| token.name == name) {
61 return Err(Refusal::Taken(name.to_owned()));
62 }
63
64 let mut secret = [0u8; 32];
65 getrandom::fill(&mut secret).expect("the operating system has a random number generator");
66 let token = format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(secret));
67
68 issued.push(Issued {
69 name: name.to_owned(),
70 created: time::OffsetDateTime::now_utc()
71 .unix_timestamp()
72 .unsigned_abs(),
73 hash: digest(&token),
74 });
75 keep(store, &issued).await?;
76
77 Ok(token)
78}
79
80pub async fn revoke(store: &Store, name: &str) -> Result<(), Refusal> {
81 let mut issued = issued(store).await?;
82 let before = issued.len();
83 issued.retain(|token| token.name != name);
84 if issued.len() == before {
85 return Err(Refusal::Unknown(name.to_owned()));
86 }
87
88 Ok(keep(store, &issued).await?)
89}
90
91impl Issued {
92 pub fn hash(&self) -> &str {
93 &self.hash
94 }
95}
96
97pub async fn holder(store: &Store, token: &str) -> Result<Option<Issued>, Error> {
98 if !token.starts_with(PREFIX) {
99 return Ok(None);
100 }
101 let hash = digest(token);
102
103 Ok(issued(store)
104 .await?
105 .into_iter()
106 .find(|issued| issued.hash == hash))
107}
108
109pub async fn still_issued(store: &Store, hash: &str) -> Result<bool, Error> {
110 Ok(issued(store)
111 .await?
112 .iter()
113 .any(|issued| issued.hash == hash))
114}