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