Add personal access token and session storage

This commit is contained in:
Olivier 'reivilibre
2025-10-02 14:34:34 +01:00
parent 293271912d
commit 8ca8d878e7
19 changed files with 967 additions and 0 deletions

View File

@@ -111,6 +111,7 @@ mod utils;
pub mod app_session;
pub mod compat;
pub mod oauth2;
pub mod personal;
pub mod policy_data;
pub mod queue;
pub mod upstream_oauth2;

View File

@@ -0,0 +1,119 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
use async_trait::async_trait;
use chrono::Duration;
use mas_data_model::{
Clock,
personal::{PersonalAccessToken, session::PersonalSession},
};
use rand_core::RngCore;
use ulid::Ulid;
use crate::repository_impl;
/// An [`PersonalAccessTokenRepository`] helps interacting with
/// [`PersonalAccessToken`] saved in the storage backend
#[async_trait]
pub trait PersonalAccessTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// Lookup an access token by its ID
///
/// Returns the access token if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the access token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalAccessToken>, Self::Error>;
/// Find an access token by its token
///
/// Returns the access token if it exists, `None` otherwise
///
/// # Parameters
///
/// * `access_token`: The token of the access token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_token(
&mut self,
access_token: &str,
) -> Result<Option<PersonalAccessToken>, Self::Error>;
/// Add a new access token to the database
///
/// Returns the newly created access token
///
/// # Parameters
///
/// * `rng`: A random number generator
/// * `clock`: The clock used to generate timestamps
/// * `session`: The session the access token is associated with
/// * `access_token`: The access token to add
/// * `expires_after`: The duration after which the access token expires. If
/// [`None`] the access token never expires
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
session: &PersonalSession,
access_token: String,
expires_after: Option<Duration>,
) -> Result<PersonalAccessToken, Self::Error>;
/// Revoke an access token
///
/// Returns the revoked access token
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `access_token`: The access token to revoke
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn revoke(
&mut self,
clock: &dyn Clock,
access_token: PersonalAccessToken,
) -> Result<PersonalAccessToken, Self::Error>;
}
repository_impl!(PersonalAccessTokenRepository:
async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalAccessToken>, Self::Error>;
async fn find_by_token(
&mut self,
access_token: &str,
) -> Result<Option<PersonalAccessToken>, Self::Error>;
async fn add(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
session: &PersonalSession,
access_token: String,
expires_after: Option<Duration>,
) -> Result<PersonalAccessToken, Self::Error>;
async fn revoke(
&mut self,
clock: &dyn Clock,
access_token: PersonalAccessToken,
) -> Result<PersonalAccessToken, Self::Error>;
);

View File

@@ -0,0 +1,13 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//! Repositories to deal with Personal Sessions and Personal Access Tokens
//! (PATs), which are sessions/access tokens created manually by users for use
//! in scripts, bots and similar applications.
mod access_token;
mod session;
pub use self::{access_token::PersonalAccessTokenRepository, session::PersonalSessionRepository};

View File

@@ -0,0 +1,101 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{Clock, Device, User, personal::session::PersonalSession};
use oauth2_types::scope::Scope;
use rand_core::RngCore;
use ulid::Ulid;
use crate::repository_impl;
/// A [`PersonalSessionRepository`] helps interacting with
/// [`PersonalSession`] saved in the storage backend
#[async_trait]
pub trait PersonalSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// Lookup a Personal session by its ID
///
/// Returns the Personal session if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the Personal session to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalSession>, Self::Error>;
/// Start a new Personal session
///
/// Returns the newly created Personal session
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `owner_user`: The user that will own the personal session
/// * `actor_user`: The user that will be represented by the personal
/// session
/// * `device`: The device ID of this session
/// * `human_name`: The human-readable name of the session provided by the
/// client or the user
/// * `scope`: The [`Scope`] of the [`PersonalSession`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
owner_user: &User,
actor_user: &User,
human_name: String,
scope: Scope,
) -> Result<PersonalSession, Self::Error>;
/// End a Personal session
///
/// Returns the ended Personal session
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `Personal_session`: The Personal session to end
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn revoke(
&mut self,
clock: &dyn Clock,
personal_session: PersonalSession,
) -> Result<PersonalSession, Self::Error>;
}
repository_impl!(PersonalSessionRepository:
async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalSession>, Self::Error>;
async fn add(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
owner_user: &User,
actor_user: &User,
human_name: String,
scope: Scope,
) -> Result<PersonalSession, Self::Error>;
async fn revoke(
&mut self,
clock: &dyn Clock,
personal_session: PersonalSession,
) -> Result<PersonalSession, Self::Error>;
);

View File

@@ -18,6 +18,7 @@ use crate::{
OAuth2AccessTokenRepository, OAuth2AuthorizationGrantRepository, OAuth2ClientRepository,
OAuth2DeviceCodeGrantRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository,
},
personal::{PersonalAccessTokenRepository, PersonalSessionRepository},
policy_data::PolicyDataRepository,
queue::{QueueJobRepository, QueueScheduleRepository, QueueWorkerRepository},
upstream_oauth2::{
@@ -214,6 +215,16 @@ pub trait RepositoryAccess: Send {
&'c mut self,
) -> Box<dyn CompatRefreshTokenRepository<Error = Self::Error> + 'c>;
/// Get a [`PersonalAccessTokenRepository`]
fn personal_access_token<'c>(
&'c mut self,
) -> Box<dyn PersonalAccessTokenRepository<Error = Self::Error> + 'c>;
/// Get a [`PersonalSessionRepository`]
fn personal_session<'c>(
&'c mut self,
) -> Box<dyn PersonalSessionRepository<Error = Self::Error> + 'c>;
/// Get a [`QueueWorkerRepository`]
fn queue_worker<'c>(&'c mut self) -> Box<dyn QueueWorkerRepository<Error = Self::Error> + 'c>;
@@ -247,6 +258,7 @@ mod impls {
OAuth2ClientRepository, OAuth2DeviceCodeGrantRepository, OAuth2RefreshTokenRepository,
OAuth2SessionRepository,
},
personal::{PersonalAccessTokenRepository, PersonalSessionRepository},
policy_data::PolicyDataRepository,
queue::{QueueJobRepository, QueueScheduleRepository, QueueWorkerRepository},
upstream_oauth2::{
@@ -458,6 +470,21 @@ mod impls {
))
}
fn personal_access_token<'c>(
&'c mut self,
) -> Box<dyn PersonalAccessTokenRepository<Error = Self::Error> + 'c> {
Box::new(MapErr::new(
self.inner.personal_access_token(),
&mut self.mapper,
))
}
fn personal_session<'c>(
&'c mut self,
) -> Box<dyn PersonalSessionRepository<Error = Self::Error> + 'c> {
Box::new(MapErr::new(self.inner.personal_session(), &mut self.mapper))
}
fn queue_worker<'c>(
&'c mut self,
) -> Box<dyn QueueWorkerRepository<Error = Self::Error> + 'c> {
@@ -610,6 +637,18 @@ mod impls {
(**self).compat_refresh_token()
}
fn personal_access_token<'c>(
&'c mut self,
) -> Box<dyn PersonalAccessTokenRepository<Error = Self::Error> + 'c> {
(**self).personal_access_token()
}
fn personal_session<'c>(
&'c mut self,
) -> Box<dyn PersonalSessionRepository<Error = Self::Error> + 'c> {
(**self).personal_session()
}
fn queue_worker<'c>(
&'c mut self,
) -> Box<dyn QueueWorkerRepository<Error = Self::Error> + 'c> {