Skip to main content

sofie_identity/tokens/
mod.rs

1mod factory;
2#[cfg(feature = "token-screens")]
3pub mod screen;
4
5use std::time::Duration;
6
7use sofie::core::Instant;
8use sofie::db::{Db, in_select};
9use sofie::http::Bearer;
10use sofie::prelude::*;
11
12pub use factory::{Issued, issue, list, revoke};
13
14use crate::AccountId;
15use crate::membership::Membership;
16
17/// A write per request would cost more than the request itself.
18const LAST_USE_PRECISION: Duration = Duration::from_secs(60);
19#[cfg(feature = "token-screens")]
20const REVEALING: &str = "sofie_identity.access_token.revealing";
21
22/// Only the digest of its text is stored; revoking deletes it softly.
23#[derive(Aggregate)]
24#[aggregate(behaviors(SoftDelete))]
25pub struct AccessToken {
26    meta: Meta,
27    #[owner]
28    account: AccountId,
29    area: String,
30    digest: String,
31    scopes: String,
32    expires_at: Instant,
33    last_used_at: Option<Instant>,
34    /// Sealed, so the token screen shows a token it issued until its owner has seen it.
35    revealing: Option<String>,
36}
37
38pub type AccessTokenId = Id<AccessToken>;
39
40impl AccessToken {
41    #[must_use]
42    pub fn account(&self) -> AccountId {
43        self.account
44    }
45
46    #[must_use]
47    pub fn area(&self) -> &str {
48        &self.area
49    }
50
51    pub fn scopes(&self) -> impl Iterator<Item = &str> {
52        self.scopes.split_whitespace()
53    }
54
55    #[must_use]
56    pub fn expires_at(&self) -> Instant {
57        self.expires_at
58    }
59
60    #[must_use]
61    pub fn last_used_at(&self) -> Option<Instant> {
62        self.last_used_at
63    }
64}
65
66/// Membership is checked here, not only at issue, so a removed member's token stops at once.
67pub(crate) async fn bearer(db: &Db, area: &str, digest: &str) -> Result<Option<Bearer>> {
68    let members = Membership::AREA.eq(area.to_owned());
69    let found = AccessToken::query()
70        .where_(AccessToken::DIGEST.eq(digest.to_owned()))
71        .where_(AccessToken::AREA.eq(area.to_owned()))
72        .where_(in_select(
73            AccessToken::ACCOUNT,
74            Membership::ACCOUNT,
75            members,
76        ))
77        .first(&db.read())
78        .await?;
79    let now = db.clock().now();
80    let Some(mut token) = found.filter(|token| token.expires_at > now) else {
81        return Ok(None);
82    };
83    record_use(db, &mut token, now).await?;
84    Ok(Some(Bearer {
85        token: token.id().uuid(),
86        principal: token.account.uuid(),
87        scopes: token.scopes().map(str::to_owned).collect(),
88    }))
89}
90
91async fn record_use(db: &Db, token: &mut AccessToken, now: Instant) -> Result<()> {
92    let recent_since = now.checked_sub(LAST_USE_PRECISION)?;
93    if token.last_used_at.is_some_and(|used| used > recent_since) {
94        return Ok(());
95    }
96    token.last_used_at = Some(now);
97    token.revealing = None;
98    match db.commit(token).await {
99        // A request with the same token recorded its use first.
100        Err(Error::Conflict) => Ok(()),
101        recorded => recorded,
102    }
103}
104
105/// What each live token may do and until when; never its digest.
106#[cfg(feature = "account-data")]
107pub(crate) async fn exported(read: &Read, account: AccountId) -> Result<sofie::serde_json::Value> {
108    let tokens = list(read, account).await?;
109    Ok(sofie::serde_json::json!(
110        tokens
111            .iter()
112            .map(|token| sofie::serde_json::json!({
113                "area": token.area,
114                "scopes": token.scopes().collect::<Vec<_>>(),
115                "expires_at": token.expires_at,
116                "last_used_at": token.last_used_at,
117            }))
118            .collect::<Vec<_>>()
119    ))
120}
121
122pub(crate) async fn revoking(read: &Read, account: AccountId) -> Result<Vec<AccessToken>> {
123    let mut tokens = list(read, account).await?;
124    for token in &mut tokens {
125        token.delete();
126    }
127    Ok(tokens)
128}