1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

storage: document all the repository traits and methods

This commit is contained in:
Quentin Gliech
2023-01-25 16:09:36 +01:00
parent d14ca156ad
commit 90dbc5d6ff
44 changed files with 1202 additions and 18 deletions

View File

@ -23,11 +23,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt};
/// An implementation of [`CompatAccessTokenRepository`] for a PostgreSQL
/// connection
pub struct PgCompatAccessTokenRepository<'c> { pub struct PgCompatAccessTokenRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgCompatAccessTokenRepository<'c> { impl<'c> PgCompatAccessTokenRepository<'c> {
/// Create a new [`PgCompatAccessTokenRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! A module containing PostgreSQL implementation of repositories for the
//! compatibility layer
mod access_token; mod access_token;
mod refresh_token; mod refresh_token;
mod session; mod session;

View File

@ -25,11 +25,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt};
/// An implementation of [`CompatRefreshTokenRepository`] for a PostgreSQL
/// connection
pub struct PgCompatRefreshTokenRepository<'c> { pub struct PgCompatRefreshTokenRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgCompatRefreshTokenRepository<'c> { impl<'c> PgCompatRefreshTokenRepository<'c> {
/// Create a new [`PgCompatRefreshTokenRepository`] from an active
/// PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -23,11 +23,14 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
/// An implementation of [`CompatSessionRepository`] for a PostgreSQL connection
pub struct PgCompatSessionRepository<'c> { pub struct PgCompatSessionRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgCompatSessionRepository<'c> { impl<'c> PgCompatSessionRepository<'c> {
/// Create a new [`PgCompatSessionRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -27,11 +27,15 @@ use crate::{
LookupResultExt, LookupResultExt,
}; };
/// An implementation of [`CompatSsoLoginRepository`] for a PostgreSQL
/// connection
pub struct PgCompatSsoLoginRepository<'c> { pub struct PgCompatSsoLoginRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgCompatSsoLoginRepository<'c> { impl<'c> PgCompatSsoLoginRepository<'c> {
/// Create a new [`PgCompatSsoLoginRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -13,25 +13,29 @@
// limitations under the License. // limitations under the License.
//! An implementation of the storage traits for a PostgreSQL database //! An implementation of the storage traits for a PostgreSQL database
//!
//! This backend uses [`sqlx`] to interact with the database. Most queries are
//! type-checked, using introspection data recorded in the `sqlx-data.json`
//! file. This file is generated by the `sqlx` CLI tool, and should be updated
//! whenever the database schema changes, or new queries are added.
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny( #![deny(
clippy::all, clippy::all,
clippy::str_to_string, clippy::str_to_string,
clippy::future_not_send, clippy::future_not_send,
rustdoc::broken_intra_doc_links rustdoc::broken_intra_doc_links,
missing_docs
)] )]
#![warn(clippy::pedantic)] #![warn(clippy::pedantic)]
#![allow( #![allow(clippy::module_name_repetitions)]
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions
)]
use sqlx::{migrate::Migrator, postgres::PgQueryResult}; use sqlx::{migrate::Migrator, postgres::PgQueryResult};
use thiserror::Error; use thiserror::Error;
use ulid::Ulid; use ulid::Ulid;
/// An extension trait for [`Result`] which adds a [`to_option`] method, useful
/// for handling "not found" errors from [`sqlx`]
trait LookupResultExt { trait LookupResultExt {
type Output; type Output;
@ -57,7 +61,11 @@ impl<T> LookupResultExt for Result<T, sqlx::Error> {
#[error(transparent)] #[error(transparent)]
pub enum DatabaseError { pub enum DatabaseError {
/// An error which came from the database itself /// An error which came from the database itself
Driver(#[from] sqlx::Error), Driver {
/// The underlying error from the database driver
#[from]
source: sqlx::Error,
},
/// An error which occured while converting the data from the database /// An error which occured while converting the data from the database
Inconsistency(#[from] DatabaseInconsistencyError), Inconsistency(#[from] DatabaseInconsistencyError),
@ -66,6 +74,7 @@ pub enum DatabaseError {
/// invalid /// invalid
#[error("Invalid database operation")] #[error("Invalid database operation")]
InvalidOperation { InvalidOperation {
/// The source of the error, if any
#[source] #[source]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>, source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}, },
@ -73,7 +82,13 @@ pub enum DatabaseError {
/// An error which happens when an operation affects not enough or too many /// An error which happens when an operation affects not enough or too many
/// rows /// rows
#[error("Expected {expected} rows to be affected, but {actual} rows were affected")] #[error("Expected {expected} rows to be affected, but {actual} rows were affected")]
RowsAffected { expected: u64, actual: u64 }, RowsAffected {
/// How many rows were expected to be affected
expected: u64,
/// How many rows were actually affected
actual: u64,
},
} }
impl DatabaseError { impl DatabaseError {
@ -100,12 +115,19 @@ impl DatabaseError {
} }
} }
/// An error which occured while converting the data from the database
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub struct DatabaseInconsistencyError { pub struct DatabaseInconsistencyError {
/// The table which was being queried
table: &'static str, table: &'static str,
/// The column which was being queried
column: Option<&'static str>, column: Option<&'static str>,
/// The row which was being queried
row: Option<Ulid>, row: Option<Ulid>,
/// The source of the error
#[source] #[source]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>, source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
} }
@ -125,6 +147,7 @@ impl std::fmt::Display for DatabaseInconsistencyError {
} }
impl DatabaseInconsistencyError { impl DatabaseInconsistencyError {
/// Create a new [`DatabaseInconsistencyError`] for the given table
#[must_use] #[must_use]
pub(crate) const fn on(table: &'static str) -> Self { pub(crate) const fn on(table: &'static str) -> Self {
Self { Self {
@ -135,18 +158,22 @@ impl DatabaseInconsistencyError {
} }
} }
/// Set the column which was being queried
#[must_use] #[must_use]
pub(crate) const fn column(mut self, column: &'static str) -> Self { pub(crate) const fn column(mut self, column: &'static str) -> Self {
self.column = Some(column); self.column = Some(column);
self self
} }
/// Set the row which was being queried
#[must_use] #[must_use]
pub(crate) const fn row(mut self, row: Ulid) -> Self { pub(crate) const fn row(mut self, row: Ulid) -> Self {
self.row = Some(row); self.row = Some(row);
self self
} }
/// Give the source of the error
#[must_use]
pub(crate) fn source<E: std::error::Error + Send + Sync + 'static>( pub(crate) fn source<E: std::error::Error + Send + Sync + 'static>(
mut self, mut self,
source: E, source: E,
@ -158,11 +185,12 @@ impl DatabaseInconsistencyError {
pub mod compat; pub mod compat;
pub mod oauth2; pub mod oauth2;
pub mod upstream_oauth2;
pub mod user;
pub(crate) mod pagination; pub(crate) mod pagination;
pub(crate) mod repository; pub(crate) mod repository;
pub(crate) mod tracing; pub(crate) mod tracing;
pub mod upstream_oauth2;
pub mod user;
pub use self::repository::PgRepository; pub use self::repository::PgRepository;

View File

@ -23,11 +23,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt};
/// An implementation of [`OAuth2AccessTokenRepository`] for a PostgreSQL
/// connection
pub struct PgOAuth2AccessTokenRepository<'c> { pub struct PgOAuth2AccessTokenRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgOAuth2AccessTokenRepository<'c> { impl<'c> PgOAuth2AccessTokenRepository<'c> {
/// Create a new [`PgOAuth2AccessTokenRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -30,11 +30,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
/// An implementation of [`OAuth2AuthorizationGrantRepository`] for a PostgreSQL
/// connection
pub struct PgOAuth2AuthorizationGrantRepository<'c> { pub struct PgOAuth2AuthorizationGrantRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgOAuth2AuthorizationGrantRepository<'c> { impl<'c> PgOAuth2AuthorizationGrantRepository<'c> {
/// Create a new [`PgOAuth2AuthorizationGrantRepository`] from an active
/// PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -39,11 +39,14 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
/// An implementation of [`OAuth2ClientRepository`] for a PostgreSQL connection
pub struct PgOAuth2ClientRepository<'c> { pub struct PgOAuth2ClientRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgOAuth2ClientRepository<'c> { impl<'c> PgOAuth2ClientRepository<'c> {
/// Create a new [`PgOAuth2ClientRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! A module containing the PostgreSQL implementations of the OAuth2-related
//! repositories
mod access_token; mod access_token;
pub mod authorization_grant; mod authorization_grant;
mod client; mod client;
mod refresh_token; mod refresh_token;
mod session; mod session;

View File

@ -23,11 +23,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, LookupResultExt};
/// An implementation of [`OAuth2RefreshTokenRepository`] for a PostgreSQL
/// connection
pub struct PgOAuth2RefreshTokenRepository<'c> { pub struct PgOAuth2RefreshTokenRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgOAuth2RefreshTokenRepository<'c> { impl<'c> PgOAuth2RefreshTokenRepository<'c> {
/// Create a new [`PgOAuth2RefreshTokenRepository`] from an active
/// PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -26,11 +26,14 @@ use crate::{
LookupResultExt, LookupResultExt,
}; };
/// An implementation of [`OAuth2SessionRepository`] for a PostgreSQL connection
pub struct PgOAuth2SessionRepository<'c> { pub struct PgOAuth2SessionRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgOAuth2SessionRepository<'c> { impl<'c> PgOAuth2SessionRepository<'c> {
/// Create a new [`PgOAuth2SessionRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -51,11 +51,19 @@ use crate::{
DatabaseError, DatabaseError,
}; };
/// An implementation of the [`Repository`] trait backed by a PostgreSQL
/// transaction.
pub struct PgRepository { pub struct PgRepository {
txn: Transaction<'static, Postgres>, txn: Transaction<'static, Postgres>,
} }
impl PgRepository { impl PgRepository {
/// Create a new [`PgRepository`] from a PostgreSQL connection pool,
/// starting a transaction.
///
/// # Errors
///
/// Returns a [`DatabaseError`] if the transaction could not be started.
pub async fn from_pool(pool: &PgPool) -> Result<Self, DatabaseError> { pub async fn from_pool(pool: &PgPool) -> Result<Self, DatabaseError> {
let txn = pool.begin().await?; let txn = pool.begin().await?;
Ok(PgRepository { txn }) Ok(PgRepository { txn })

View File

@ -14,6 +14,8 @@
use tracing::Span; use tracing::Span;
/// An extension trait for [`sqlx::Execute`] that records the SQL statement as
/// `db.statement` in a tracing span
pub trait ExecuteExt<'q, DB>: Sized { pub trait ExecuteExt<'q, DB>: Sized {
/// Records the statement as `db.statement` in the current span /// Records the statement as `db.statement` in the current span
fn traced(self) -> Self { fn traced(self) -> Self {

View File

@ -23,11 +23,15 @@ use uuid::Uuid;
use crate::{pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, LookupResultExt}; use crate::{pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, LookupResultExt};
/// An implementation of [`UpstreamOAuthLinkRepository`] for a PostgreSQL
/// connection
pub struct PgUpstreamOAuthLinkRepository<'c> { pub struct PgUpstreamOAuthLinkRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUpstreamOAuthLinkRepository<'c> { impl<'c> PgUpstreamOAuthLinkRepository<'c> {
/// Create a new [`PgUpstreamOAuthLinkRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! A module containing the PostgreSQL implementation of the repositories
//! related to the upstream OAuth 2.0 providers
mod link; mod link;
mod provider; mod provider;
mod session; mod session;
@ -178,6 +181,8 @@ mod tests {
assert_eq!(links.edges[0].user_id, Some(user.id)); assert_eq!(links.edges[0].user_id, Some(user.id));
} }
/// Test that the pagination works as expected in the upstream OAuth
/// provider repository
#[sqlx::test(migrator = "crate::MIGRATOR")] #[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_provider_repository_pagination(pool: PgPool) { async fn test_provider_repository_pagination(pool: PgPool) {
const ISSUER: &str = "https://example.com/"; const ISSUER: &str = "https://example.com/";

View File

@ -28,11 +28,15 @@ use crate::{
LookupResultExt, LookupResultExt,
}; };
/// An implementation of [`UpstreamOAuthProviderRepository`] for a PostgreSQL
/// connection
pub struct PgUpstreamOAuthProviderRepository<'c> { pub struct PgUpstreamOAuthProviderRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUpstreamOAuthProviderRepository<'c> { impl<'c> PgUpstreamOAuthProviderRepository<'c> {
/// Create a new [`PgUpstreamOAuthProviderRepository`] from an active
/// PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -26,11 +26,15 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
/// An implementation of [`UpstreamOAuthSessionRepository`] for a PostgreSQL
/// connection
pub struct PgUpstreamOAuthSessionRepository<'c> { pub struct PgUpstreamOAuthSessionRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUpstreamOAuthSessionRepository<'c> { impl<'c> PgUpstreamOAuthSessionRepository<'c> {
/// Create a new [`PgUpstreamOAuthSessionRepository`] from an active
/// PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -27,11 +27,14 @@ use crate::{
LookupResultExt, LookupResultExt,
}; };
/// An implementation of [`UserEmailRepository`] for a PostgreSQL connection
pub struct PgUserEmailRepository<'c> { pub struct PgUserEmailRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUserEmailRepository<'c> { impl<'c> PgUserEmailRepository<'c> {
/// Create a new [`PgUserEmailRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! A module containing the PostgreSQL implementation of the user-related
//! repositories
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mas_data_model::User; use mas_data_model::User;
@ -35,11 +38,13 @@ pub use self::{
session::PgBrowserSessionRepository, session::PgBrowserSessionRepository,
}; };
/// An implementation of [`UserRepository`] for a PostgreSQL connection
pub struct PgUserRepository<'c> { pub struct PgUserRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUserRepository<'c> { impl<'c> PgUserRepository<'c> {
/// Create a new [`PgUserRepository`] from an active PostgreSQL connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -23,11 +23,14 @@ use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt}; use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
/// An implementation of [`UserPasswordRepository`] for a PostgreSQL connection
pub struct PgUserPasswordRepository<'c> { pub struct PgUserPasswordRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgUserPasswordRepository<'c> { impl<'c> PgUserPasswordRepository<'c> {
/// Create a new [`PgUserPasswordRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -26,11 +26,15 @@ use crate::{
LookupResultExt, LookupResultExt,
}; };
/// An implementation of [`BrowserSessionRepository`] for a PostgreSQL
/// connection
pub struct PgBrowserSessionRepository<'c> { pub struct PgBrowserSessionRepository<'c> {
conn: &'c mut PgConnection, conn: &'c mut PgConnection,
} }
impl<'c> PgBrowserSessionRepository<'c> { impl<'c> PgBrowserSessionRepository<'c> {
/// Create a new [`PgBrowserSessionRepository`] from an active PostgreSQL
/// connection
pub fn new(conn: &'c mut PgConnection) -> Self { pub fn new(conn: &'c mut PgConnection) -> Self {
Self { conn } Self { conn }
} }

View File

@ -253,6 +253,7 @@ async fn test_user_email_repo(pool: PgPool) {
repo.save().await.unwrap(); repo.save().await.unwrap();
} }
/// Test the user password repository implementation.
#[sqlx::test(migrator = "crate::MIGRATOR")] #[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_user_password_repo(pool: PgPool) { async fn test_user_password_repo(pool: PgPool) {
const USERNAME: &str = "john"; const USERNAME: &str = "john";

View File

@ -20,20 +20,58 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// A [`CompatAccessTokenRepository`] helps interacting with
/// [`CompatAccessToken`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait CompatAccessTokenRepository: Send + Sync { pub trait CompatAccessTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a compat access token by its ID /// Lookup a compat access token by its ID
///
/// Returns the compat access token if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the compat access token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatAccessToken>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatAccessToken>, Self::Error>;
/// Find a compat access token by its token /// Find a compat access token by its token
///
/// Returns the compat access token if found, `None` otherwise
///
/// # Parameters
///
/// * `access_token`: The token of the compat access token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_token( async fn find_by_token(
&mut self, &mut self,
access_token: &str, access_token: &str,
) -> Result<Option<CompatAccessToken>, Self::Error>; ) -> Result<Option<CompatAccessToken>, Self::Error>;
/// Add a new compat access token to the database /// Add a new compat access token to the database
///
/// Returns the newly created compat access token
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `compat_session`: The compat session associated with the access token
/// * `token`: The token of the access token
/// * `expires_after`: The duration after which the access token expires, if
/// specified
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -44,6 +82,13 @@ pub trait CompatAccessTokenRepository: Send + Sync {
) -> Result<CompatAccessToken, Self::Error>; ) -> Result<CompatAccessToken, Self::Error>;
/// Set the expiration time of the compat access token to now /// Set the expiration time of the compat access token to now
///
/// Returns the expired compat access token
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `compat_access_token`: The compat access token to expire
async fn expire( async fn expire(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Repositories to interact with entities of the compatibility layer
mod access_token; mod access_token;
mod refresh_token; mod refresh_token;
mod session; mod session;

View File

@ -19,20 +19,55 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// A [`CompatRefreshTokenRepository`] helps interacting with
/// [`CompatRefreshToken`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait CompatRefreshTokenRepository: Send + Sync { pub trait CompatRefreshTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a compat refresh token by its ID /// Lookup a compat refresh token by its ID
///
/// Returns the compat refresh token if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the compat refresh token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatRefreshToken>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatRefreshToken>, Self::Error>;
/// Find a compat refresh token by its token /// Find a compat refresh token by its token
///
/// Returns the compat refresh token if found, `None` otherwise
///
/// # Parameters
///
/// * `refresh_token`: The token of the compat refresh token to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_token( async fn find_by_token(
&mut self, &mut self,
refresh_token: &str, refresh_token: &str,
) -> Result<Option<CompatRefreshToken>, Self::Error>; ) -> Result<Option<CompatRefreshToken>, Self::Error>;
/// Add a new compat refresh token to the database /// Add a new compat refresh token to the database
///
/// Returns the newly created compat refresh token
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `compat_session`: The compat session associated with this refresh
/// token
/// * `compat_access_token`: The compat access token created alongside this
/// refresh token
/// * `token`: The token of the refresh token
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -43,6 +78,17 @@ pub trait CompatRefreshTokenRepository: Send + Sync {
) -> Result<CompatRefreshToken, Self::Error>; ) -> Result<CompatRefreshToken, Self::Error>;
/// Consume a compat refresh token /// Consume a compat refresh token
///
/// Returns the consumed compat refresh token
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `compat_refresh_token`: The compat refresh token to consume
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn consume( async fn consume(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -19,14 +19,40 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// A [`CompatSessionRepository`] helps interacting with
/// [`CompatSessionRepository`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait CompatSessionRepository: Send + Sync { pub trait CompatSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a compat session by its ID /// Lookup a compat session by its ID
///
/// Returns the compat session if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the compat session to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
/// Start a new compat session /// Start a new compat session
///
/// Returns the newly created compat session
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `user`: The user to create the compat session for
/// * `device`: The device ID of this session
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -36,6 +62,17 @@ pub trait CompatSessionRepository: Send + Sync {
) -> Result<CompatSession, Self::Error>; ) -> Result<CompatSession, Self::Error>;
/// End a compat session /// End a compat session
///
/// Returns the ended compat session
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `compat_session`: The compat session to end
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn finish( async fn finish(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -20,20 +20,56 @@ use url::Url;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`CompatSsoLoginRepository`] helps interacting with
/// [`CompatSsoLoginRepository`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait CompatSsoLoginRepository: Send + Sync { pub trait CompatSsoLoginRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a compat SSO login by its ID /// Lookup a compat SSO login by its ID
///
/// Returns the compat SSO login if it exists, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the compat SSO login to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSsoLogin>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSsoLogin>, Self::Error>;
/// Find a compat SSO login by its login token /// Find a compat SSO login by its login token
///
/// Returns the compat SSO login if found, `None` otherwise
///
/// # Parameters
///
/// * `login_token`: The login token of the compat SSO login to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_token( async fn find_by_token(
&mut self, &mut self,
login_token: &str, login_token: &str,
) -> Result<Option<CompatSsoLogin>, Self::Error>; ) -> Result<Option<CompatSsoLogin>, Self::Error>;
/// Start a new compat SSO login token /// Start a new compat SSO login token
///
/// Returns the newly created compat SSO login
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate the timestamps
/// * `login_token`: The login token given to the client
/// * `redirect_uri`: The redirect URI given by the client
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -43,6 +79,19 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>; ) -> Result<CompatSsoLogin, Self::Error>;
/// Fulfill a compat SSO login by providing a compat session /// Fulfill a compat SSO login by providing a compat session
///
/// Returns the fulfilled compat SSO login
///
/// # Parameters
///
/// * `clock`: The clock used to generate the timestamps
/// * `compat_sso_login`: The compat SSO login to fulfill
/// * `compat_session`: The compat session to associate with the compat SSO
/// login
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn fulfill( async fn fulfill(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -51,6 +100,17 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>; ) -> Result<CompatSsoLogin, Self::Error>;
/// Mark a compat SSO login as exchanged /// Mark a compat SSO login as exchanged
///
/// Returns the exchanged compat SSO login
///
/// # Parameters
///
/// * `clock`: The clock used to generate the timestamps
/// * `compat_sso_login`: The compat SSO login to mark as exchanged
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn exchange( async fn exchange(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -58,6 +118,15 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>; ) -> Result<CompatSsoLogin, Self::Error>;
/// Get a paginated list of compat SSO logins for a user /// Get a paginated list of compat SSO logins for a user
///
/// # Parameters
///
/// * `user`: The user to get the compat SSO logins for
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated( async fn list_paginated(
&mut self, &mut self,
user: &User, user: &User,

View File

@ -19,14 +19,11 @@
clippy::all, clippy::all,
clippy::str_to_string, clippy::str_to_string,
clippy::future_not_send, clippy::future_not_send,
rustdoc::broken_intra_doc_links rustdoc::broken_intra_doc_links,
missing_docs
)] )]
#![warn(clippy::pedantic)] #![warn(clippy::pedantic)]
#![allow( #![allow(clippy::module_name_repetitions)]
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions
)]
use rand_core::CryptoRngCore; use rand_core::CryptoRngCore;
@ -103,5 +100,8 @@ macro_rules! repository_impl {
}; };
} }
/// A boxed [`Clock`]
pub type BoxClock = Box<dyn Clock + Send>; pub type BoxClock = Box<dyn Clock + Send>;
/// A boxed random number generator
pub type BoxRng = Box<dyn CryptoRngCore + Send>; pub type BoxRng = Box<dyn CryptoRngCore + Send>;

View File

@ -20,20 +20,57 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// An [`OAuth2AccessTokenRepository`] helps interacting with [`AccessToken`]
/// saved in the storage backend
#[async_trait] #[async_trait]
pub trait OAuth2AccessTokenRepository: Send + Sync { pub trait OAuth2AccessTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an access token by its ID /// 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<AccessToken>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<AccessToken>, Self::Error>;
/// Find an access token by its token /// 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( async fn find_by_token(
&mut self, &mut self,
access_token: &str, access_token: &str,
) -> Result<Option<AccessToken>, Self::Error>; ) -> Result<Option<AccessToken>, Self::Error>;
/// Add a new access token to the database /// 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
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -44,6 +81,17 @@ pub trait OAuth2AccessTokenRepository: Send + Sync {
) -> Result<AccessToken, Self::Error>; ) -> Result<AccessToken, Self::Error>;
/// Revoke an access token /// 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( async fn revoke(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -51,6 +99,16 @@ pub trait OAuth2AccessTokenRepository: Send + Sync {
) -> Result<AccessToken, Self::Error>; ) -> Result<AccessToken, Self::Error>;
/// Cleanup expired access tokens /// Cleanup expired access tokens
///
/// Returns the number of access tokens that were cleaned up
///
/// # Parameters
///
/// * `clock`: The clock used to get the current time
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn cleanup_expired(&mut self, clock: &dyn Clock) -> Result<usize, Self::Error>; async fn cleanup_expired(&mut self, clock: &dyn Clock) -> Result<usize, Self::Error>;
} }

View File

@ -23,10 +23,38 @@ use url::Url;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// An [`OAuth2AuthorizationGrantRepository`] helps interacting with
/// [`AuthorizationGrant`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait OAuth2AuthorizationGrantRepository: Send + Sync { pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Create a new authorization grant
///
/// Returns the newly created authorization grant
///
/// # Parameters
///
/// * `rng`: A random number generator
/// * `clock`: The clock used to generate timestamps
/// * `client`: The client that requested the authorization grant
/// * `redirect_uri`: The redirect URI the client requested
/// * `scope`: The scope the client requested
/// * `code`: The authorization code used by this grant, if the `code`
/// `response_type` was requested
/// * `state`: The state the client sent, if set
/// * `nonce`: The nonce the client sent, if set
/// * `max_age`: The maximum age since the user last authenticated, if asked
/// by the client
/// * `response_mode`: The response mode the client requested
/// * `response_type_id_token`: Whether the `id_token` `response_type` was
/// requested
/// * `requires_consent`: Whether the client explicitly requested consent
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn add( async fn add(
&mut self, &mut self,
@ -44,11 +72,47 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
requires_consent: bool, requires_consent: bool,
) -> Result<AuthorizationGrant, Self::Error>; ) -> Result<AuthorizationGrant, Self::Error>;
/// Lookup an authorization grant by its ID
///
/// Returns the authorization grant if found, `None` otherwise
///
/// # Parameters
///
/// * `id`: The ID of the authorization grant to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<AuthorizationGrant>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<AuthorizationGrant>, Self::Error>;
/// Find an authorization grant by its code
///
/// Returns the authorization grant if found, `None` otherwise
///
/// # Parameters
///
/// * `code`: The code of the authorization grant to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_code(&mut self, code: &str) async fn find_by_code(&mut self, code: &str)
-> Result<Option<AuthorizationGrant>, Self::Error>; -> Result<Option<AuthorizationGrant>, Self::Error>;
/// Fulfill an authorization grant, by giving the [`Session`] that it
/// created
///
/// Returns the updated authorization grant
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `session`: The session that was created using this authorization grant
/// * `authorization_grant`: The authorization grant to fulfill
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn fulfill( async fn fulfill(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -56,12 +120,35 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
authorization_grant: AuthorizationGrant, authorization_grant: AuthorizationGrant,
) -> Result<AuthorizationGrant, Self::Error>; ) -> Result<AuthorizationGrant, Self::Error>;
/// Mark an authorization grant as exchanged
///
/// Returns the updated authorization grant
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `authorization_grant`: The authorization grant to mark as exchanged
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn exchange( async fn exchange(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
authorization_grant: AuthorizationGrant, authorization_grant: AuthorizationGrant,
) -> Result<AuthorizationGrant, Self::Error>; ) -> Result<AuthorizationGrant, Self::Error>;
/// Unset the `requires_consent` flag on an authorization grant
///
/// Returns the updated authorization grant
///
/// # Parameters
///
/// * `authorization_grant`: The authorization grant to update
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn give_consent( async fn give_consent(
&mut self, &mut self,
authorization_grant: AuthorizationGrant, authorization_grant: AuthorizationGrant,

View File

@ -25,22 +25,82 @@ use url::Url;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the
/// storage backend
#[async_trait] #[async_trait]
pub trait OAuth2ClientRepository: Send + Sync { pub trait OAuth2ClientRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an OAuth2 client by its ID
///
/// Returns `None` if the client does not exist
///
/// # Parameters
///
/// * `id`: The ID of the client to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error>;
/// Find an OAuth2 client by its client ID
async fn find_by_client_id(&mut self, client_id: &str) -> Result<Option<Client>, Self::Error> { async fn find_by_client_id(&mut self, client_id: &str) -> Result<Option<Client>, Self::Error> {
let Ok(id) = client_id.parse() else { return Ok(None) }; let Ok(id) = client_id.parse() else { return Ok(None) };
self.lookup(id).await self.lookup(id).await
} }
/// Load a batch of OAuth2 clients by their IDs
///
/// Returns a map of client IDs to clients. If a client does not exist, it
/// is not present in the map.
///
/// # Parameters
///
/// * `ids`: The IDs of the clients to load
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn load_batch( async fn load_batch(
&mut self, &mut self,
ids: BTreeSet<Ulid>, ids: BTreeSet<Ulid>,
) -> Result<BTreeMap<Ulid, Client>, Self::Error>; ) -> Result<BTreeMap<Ulid, Client>, Self::Error>;
/// Add a new OAuth2 client
///
/// Returns the client that was added
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `redirect_uris`: The list of redirect URIs used by this client
/// * `encrypted_client_secret`: The encrypted client secret, if any
/// * `grant_types`: The list of grant types this client can use
/// * `contacts`: The list of contacts for this client
/// * `client_name`: The human-readable name of this client, if given
/// * `logo_uri`: The URI of the logo of this client, if given
/// * `client_uri`: The URI of a website of this client, if given
/// * `policy_uri`: The URI of the privacy policy of this client, if given
/// * `tos_uri`: The URI of the terms of service of this client, if given
/// * `jwks_uri`: The URI of the JWKS of this client, if given
/// * `jwks`: The JWKS of this client, if given
/// * `id_token_signed_response_alg`: The algorithm used to sign the ID
/// token
/// * `userinfo_signed_response_alg`: The algorithm used to sign the user
/// info. If none, the user info endpoint will not sign the response
/// * `token_endpoint_auth_method`: The authentication method used by this
/// client when calling the token endpoint
/// * `token_endpoint_auth_signing_alg`: The algorithm used to sign the JWT
/// when using the `client_secret_jwt` or `private_key_jwt` authentication
/// methods
/// * `initiate_login_uri`: The URI used to initiate a login, if given
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn add( async fn add(
&mut self, &mut self,
@ -64,6 +124,24 @@ pub trait OAuth2ClientRepository: Send + Sync {
initiate_login_uri: Option<Url>, initiate_login_uri: Option<Url>,
) -> Result<Client, Self::Error>; ) -> Result<Client, Self::Error>;
/// Add or replace a client from the configuration
///
/// Returns the client that was added or replaced
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `client_id`: The client ID
/// * `client_auth_method`: The authentication method this client uses
/// * `encrypted_client_secret`: The encrypted client secret, if any
/// * `jwks`: The client JWKS, if any
/// * `jwks_uri`: The client JWKS URI, if any
/// * `redirect_uris`: The list of redirect URIs used by this client
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn add_from_config( async fn add_from_config(
&mut self, &mut self,
@ -77,12 +155,36 @@ pub trait OAuth2ClientRepository: Send + Sync {
redirect_uris: Vec<Url>, redirect_uris: Vec<Url>,
) -> Result<Client, Self::Error>; ) -> Result<Client, Self::Error>;
/// Get the list of scopes that the user has given consent for the given
/// client
///
/// # Parameters
///
/// * `client`: The client to get the consent for
/// * `user`: The user to get the consent for
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn get_consent_for_user( async fn get_consent_for_user(
&mut self, &mut self,
client: &Client, client: &Client,
user: &User, user: &User,
) -> Result<Scope, Self::Error>; ) -> Result<Scope, Self::Error>;
/// Give consent for a set of scopes for the given client and user
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `client`: The client to give the consent for
/// * `user`: The user to give the consent for
/// * `scope`: The scope to give consent for
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn give_consent_for_user( async fn give_consent_for_user(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),

View File

@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Repositories to interact with entities related to the OAuth 2.0 protocol
mod access_token; mod access_token;
pub mod authorization_grant; mod authorization_grant;
mod client; mod client;
mod refresh_token; mod refresh_token;
mod session; mod session;

View File

@ -19,20 +19,58 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// An [`OAuth2RefreshTokenRepository`] helps interacting with [`RefreshToken`]
/// saved in the storage backend
#[async_trait] #[async_trait]
pub trait OAuth2RefreshTokenRepository: Send + Sync { pub trait OAuth2RefreshTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a refresh token by its ID /// Lookup a refresh token by its ID
///
/// Returns `None` if no [`RefreshToken`] was found
///
/// # Parameters
///
/// * `id`: The ID of the [`RefreshToken`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<RefreshToken>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<RefreshToken>, Self::Error>;
/// Find a refresh token by its token /// Find a refresh token by its token
///
/// Returns `None` if no [`RefreshToken`] was found
///
/// # Parameters
///
/// * `token`: The token of the [`RefreshToken`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_token( async fn find_by_token(
&mut self, &mut self,
refresh_token: &str, refresh_token: &str,
) -> Result<Option<RefreshToken>, Self::Error>; ) -> Result<Option<RefreshToken>, Self::Error>;
/// Add a new refresh token to the database /// Add a new refresh token to the database
///
/// Returns the newly created [`RefreshToken`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `session`: The [`Session`] in which to create the [`RefreshToken`]
/// * `access_token`: The [`AccessToken`] created alongside this
/// [`RefreshToken`]
/// * `refresh_token`: The refresh token to store
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -43,6 +81,18 @@ pub trait OAuth2RefreshTokenRepository: Send + Sync {
) -> Result<RefreshToken, Self::Error>; ) -> Result<RefreshToken, Self::Error>;
/// Consume a refresh token /// Consume a refresh token
///
/// Returns the updated [`RefreshToken`]
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `refresh_token`: The [`RefreshToken`] to consume
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails, or if the
/// token was already consumed
async fn consume( async fn consume(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -19,12 +19,41 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`OAuth2SessionRepository`] helps interacting with [`Session`]
/// saved in the storage backend
#[async_trait] #[async_trait]
pub trait OAuth2SessionRepository: Send + Sync { pub trait OAuth2SessionRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an [`Session`] by its ID
///
/// Returns `None` if no [`Session`] was found
///
/// # Parameters
///
/// * `id`: The ID of the [`Session`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<Session>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<Session>, Self::Error>;
/// Create a new [`Session`] from an [`AuthorizationGrant`]
///
/// Returns the newly created [`Session`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `grant`: The [`AuthorizationGrant`] to create the [`Session`] from
/// * `user_session`: The [`BrowserSession`] of the user which completed the
/// authorization
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn create_from_grant( async fn create_from_grant(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -33,9 +62,31 @@ pub trait OAuth2SessionRepository: Send + Sync {
user_session: &BrowserSession, user_session: &BrowserSession,
) -> Result<Session, Self::Error>; ) -> Result<Session, Self::Error>;
/// Mark a [`Session`] as finished
///
/// Returns the updated [`Session`]
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `session`: The [`Session`] to mark as finished
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn finish(&mut self, clock: &dyn Clock, session: Session) async fn finish(&mut self, clock: &dyn Clock, session: Session)
-> Result<Session, Self::Error>; -> Result<Session, Self::Error>;
/// Get a paginated list of [`Session`]s for a [`User`]
///
/// # Parameters
///
/// * `user`: The [`User`] to get the [`Session`]s for
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated( async fn list_paginated(
&mut self, &mut self,
user: &User, user: &User,

View File

@ -22,17 +22,29 @@ use ulid::Ulid;
#[error("Either 'first' or 'last' must be specified")] #[error("Either 'first' or 'last' must be specified")]
pub struct InvalidPagination; pub struct InvalidPagination;
/// Pagination parameters
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pagination { pub struct Pagination {
/// The cursor to start from
pub before: Option<Ulid>, pub before: Option<Ulid>,
/// The cursor to end at
pub after: Option<Ulid>, pub after: Option<Ulid>,
/// The maximum number of items to return
pub count: usize, pub count: usize,
/// In which direction to paginate
pub direction: PaginationDirection, pub direction: PaginationDirection,
} }
/// The direction to paginate
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaginationDirection { pub enum PaginationDirection {
/// Paginate forward
Forward, Forward,
/// Paginate backward
Backward, Backward,
} }
@ -124,13 +136,24 @@ impl Pagination {
} }
} }
/// A page of results returned by a paginated query
pub struct Page<T> { pub struct Page<T> {
/// When paginating forwards, this is true if there are more items after
pub has_next_page: bool, pub has_next_page: bool,
/// When paginating backwards, this is true if there are more items before
pub has_previous_page: bool, pub has_previous_page: bool,
/// The items in the page
pub edges: Vec<T>, pub edges: Vec<T>,
} }
impl<T> Page<T> { impl<T> Page<T> {
/// Map the items in this page with the given function
///
/// # Parameters
///
/// * `f`: The function to map the items with
#[must_use] #[must_use]
pub fn map<F, T2>(self, f: F) -> Page<T2> pub fn map<F, T2>(self, f: F) -> Page<T2>
where where
@ -144,6 +167,15 @@ impl<T> Page<T> {
} }
} }
/// Try to map the items in this page with the given fallible function
///
/// # Parameters
///
/// * `f`: The fallible function to map the items with
///
/// # Errors
///
/// Returns the first error encountered while mapping the items
pub fn try_map<F, E, T2>(self, f: F) -> Result<Page<T2>, E> pub fn try_map<F, E, T2>(self, f: F) -> Result<Page<T2>, E>
where where
F: FnMut(T) -> Result<T2, E>, F: FnMut(T) -> Result<T2, E>,

View File

@ -19,14 +19,39 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`UpstreamOAuthLinkRepository`] helps interacting with
/// [`UpstreamOAuthLink`] with the storage backend
#[async_trait] #[async_trait]
pub trait UpstreamOAuthLinkRepository: Send + Sync { pub trait UpstreamOAuthLinkRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an upstream OAuth link by its ID /// Lookup an upstream OAuth link by its ID
///
/// Returns `None` if the link does not exist
///
/// # Parameters
///
/// * `id`: The ID of the upstream OAuth link to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthLink>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
/// Find an upstream OAuth link for a provider by its subject /// Find an upstream OAuth link for a provider by its subject
///
/// Returns `None` if no matching upstream OAuth link was found
///
/// # Parameters
///
/// * `upstream_oauth_provider`: The upstream OAuth provider on which to
/// find the link
/// * `subject`: The subject of the upstream OAuth link to find
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_subject( async fn find_by_subject(
&mut self, &mut self,
upstream_oauth_provider: &UpstreamOAuthProvider, upstream_oauth_provider: &UpstreamOAuthProvider,
@ -34,6 +59,20 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<Option<UpstreamOAuthLink>, Self::Error>; ) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
/// Add a new upstream OAuth link /// Add a new upstream OAuth link
///
/// Returns the newly created upstream OAuth link
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `upsream_oauth_provider`: The upstream OAuth provider for which to
/// create the link
/// * `subject`: The subject of the upstream OAuth link to create
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -43,6 +82,17 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<UpstreamOAuthLink, Self::Error>; ) -> Result<UpstreamOAuthLink, Self::Error>;
/// Associate an upstream OAuth link to a user /// Associate an upstream OAuth link to a user
///
/// Returns the updated upstream OAuth link
///
/// # Parameters
///
/// * `upstream_oauth_link`: The upstream OAuth link to update
/// * `user`: The user to associate to the upstream OAuth link
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn associate_to_user( async fn associate_to_user(
&mut self, &mut self,
upstream_oauth_link: &UpstreamOAuthLink, upstream_oauth_link: &UpstreamOAuthLink,
@ -50,6 +100,15 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<(), Self::Error>; ) -> Result<(), Self::Error>;
/// Get a paginated list of upstream OAuth links on a user /// Get a paginated list of upstream OAuth links on a user
///
/// # Parameters
///
/// * `user`: The user for which to get the upstream OAuth links
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated( async fn list_paginated(
&mut self, &mut self,
user: &User, user: &User,

View File

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Repositories to interact with entities related to the upstream OAuth 2.0
//! providers
mod link; mod link;
mod provider; mod provider;
mod session; mod session;

View File

@ -21,14 +21,47 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`UpstreamOAuthProviderRepository`] helps interacting with
/// [`UpstreamOAuthProvider`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait UpstreamOAuthProviderRepository: Send + Sync { pub trait UpstreamOAuthProviderRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an upstream OAuth provider by its ID /// Lookup an upstream OAuth provider by its ID
///
/// Returns `None` if the provider was not found
///
/// # Parameters
///
/// * `id`: The ID of the provider to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthProvider>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthProvider>, Self::Error>;
/// Add a new upstream OAuth provider /// Add a new upstream OAuth provider
///
/// Returns the newly created provider
///
/// # Parameters
///
/// * `rng`: A random number generator
/// * `clock`: The clock used to generate timestamps
/// * `issuer`: The OIDC issuer of the provider
/// * `scope`: The scope to request during the authorization flow
/// * `token_endpoint_auth_method`: The token endpoint authentication method
/// * `token_endpoint_auth_signing_alg`: The JWT signing algorithm to use
/// when then `client_secret_jwt` or `private_key_jwt` authentication
/// methods are used
/// * `client_id`: The client ID to use when authenticating to the upstream
/// * `encrypted_client_secret`: The encrypted client secret to use when
/// authenticating to the upstream
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn add( async fn add(
&mut self, &mut self,
@ -43,12 +76,24 @@ pub trait UpstreamOAuthProviderRepository: Send + Sync {
) -> Result<UpstreamOAuthProvider, Self::Error>; ) -> Result<UpstreamOAuthProvider, Self::Error>;
/// Get a paginated list of upstream OAuth providers /// Get a paginated list of upstream OAuth providers
///
/// # Parameters
///
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated( async fn list_paginated(
&mut self, &mut self,
pagination: Pagination, pagination: Pagination,
) -> Result<Page<UpstreamOAuthProvider>, Self::Error>; ) -> Result<Page<UpstreamOAuthProvider>, Self::Error>;
/// Get all upstream OAuth providers /// Get all upstream OAuth providers
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn all(&mut self) -> Result<Vec<UpstreamOAuthProvider>, Self::Error>; async fn all(&mut self) -> Result<Vec<UpstreamOAuthProvider>, Self::Error>;
} }

View File

@ -19,17 +19,48 @@ use ulid::Ulid;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// An [`UpstreamOAuthSessionRepository`] helps interacting with
/// [`UpstreamOAuthAuthorizationSession`] saved in the storage backend
#[async_trait] #[async_trait]
pub trait UpstreamOAuthSessionRepository: Send + Sync { pub trait UpstreamOAuthSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a session by its ID /// Lookup a session by its ID
///
/// Returns `None` if the session does not exist
///
/// # Parameters
///
/// * `id`: the ID of the session to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup( async fn lookup(
&mut self, &mut self,
id: Ulid, id: Ulid,
) -> Result<Option<UpstreamOAuthAuthorizationSession>, Self::Error>; ) -> Result<Option<UpstreamOAuthAuthorizationSession>, Self::Error>;
/// Add a session to the database /// Add a session to the database
///
/// Returns the newly created session
///
/// # Parameters
///
/// * `rng`: the random number generator to use
/// * `clock`: the clock source
/// * `upstream_oauth_provider`: the upstream OAuth provider for which to
/// create the session
/// * `state`: the authorization grant `state` parameter sent to the
/// upstream OAuth provider
/// * `code_challenge_verifier`: the code challenge verifier used in this
/// session, if PKCE is being used
/// * `nonce`: the `nonce` used in this session
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -41,6 +72,20 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>; ) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
/// Mark a session as completed and associate the given link /// Mark a session as completed and associate the given link
///
/// Returns the updated session
///
/// # Parameters
///
/// * `clock`: the clock source
/// * `upstream_oauth_authorization_session`: the session to update
/// * `upstream_oauth_link`: the link to associate with the session
/// * `id_token`: the ID token returned by the upstream OAuth provider, if
/// present
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn complete_with_link( async fn complete_with_link(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -50,6 +95,17 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>; ) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
/// Mark a session as consumed /// Mark a session as consumed
///
/// Returns the updated session
///
/// # Parameters
///
/// * `clock`: the clock source
/// * `upstream_oauth_authorization_session`: the session to consume
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn consume( async fn consume(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -19,22 +19,105 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`UserEmailRepository`] helps interacting with [`UserEmail`] saved in the
/// storage backend
#[async_trait] #[async_trait]
pub trait UserEmailRepository: Send + Sync { pub trait UserEmailRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup an [`UserEmail`] by its ID
///
/// Returns `None` if no [`UserEmail`] was found
///
/// # Parameters
///
/// * `id`: The ID of the [`UserEmail`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<UserEmail>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<UserEmail>, Self::Error>;
/// Lookup an [`UserEmail`] by its email address for a [`User`]
///
/// Returns `None` if no matching [`UserEmail`] was found
///
/// # Parameters
///
/// * `user`: The [`User`] for whom to lookup the [`UserEmail`]
/// * `email`: The email address to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>; async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>;
/// Get the primary [`UserEmail`] of a [`User`]
///
/// Returns `None` if no the user has no primary [`UserEmail`]
///
/// # Parameters
///
/// * `user`: The [`User`] for whom to lookup the primary [`UserEmail`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn get_primary(&mut self, user: &User) -> Result<Option<UserEmail>, Self::Error>; async fn get_primary(&mut self, user: &User) -> Result<Option<UserEmail>, Self::Error>;
/// Get all [`UserEmail`] of a [`User`]
///
/// # Parameters
///
/// * `user`: The [`User`] for whom to lookup the [`UserEmail`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn all(&mut self, user: &User) -> Result<Vec<UserEmail>, Self::Error>; async fn all(&mut self, user: &User) -> Result<Vec<UserEmail>, Self::Error>;
/// List [`UserEmail`] of a [`User`] with the given pagination
///
/// # Parameters
///
/// * `user`: The [`User`] for whom to lookup the [`UserEmail`]
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated( async fn list_paginated(
&mut self, &mut self,
user: &User, user: &User,
pagination: Pagination, pagination: Pagination,
) -> Result<Page<UserEmail>, Self::Error>; ) -> Result<Page<UserEmail>, Self::Error>;
/// Count the [`UserEmail`] of a [`User`]
///
/// # Parameters
///
/// * `user`: The [`User`] for whom to count the [`UserEmail`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn count(&mut self, user: &User) -> Result<usize, Self::Error>; async fn count(&mut self, user: &User) -> Result<usize, Self::Error>;
/// Create a new [`UserEmail`] for a [`User`]
///
/// Returns the newly created [`UserEmail`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock to use
/// * `user`: The [`User`] for whom to create the [`UserEmail`]
/// * `email`: The email address of the [`UserEmail`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -42,16 +125,61 @@ pub trait UserEmailRepository: Send + Sync {
user: &User, user: &User,
email: String, email: String,
) -> Result<UserEmail, Self::Error>; ) -> Result<UserEmail, Self::Error>;
/// Delete a [`UserEmail`]
///
/// # Parameters
///
/// * `user_email`: The [`UserEmail`] to delete
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error>; async fn remove(&mut self, user_email: UserEmail) -> Result<(), Self::Error>;
/// Mark a [`UserEmail`] as verified
///
/// Returns the updated [`UserEmail`]
///
/// # Parameters
///
/// * `clock`: The clock to use
/// * `user_email`: The [`UserEmail`] to mark as verified
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn mark_as_verified( async fn mark_as_verified(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
user_email: UserEmail, user_email: UserEmail,
) -> Result<UserEmail, Self::Error>; ) -> Result<UserEmail, Self::Error>;
/// Mark a [`UserEmail`] as primary
///
/// # Parameters
///
/// * `user_email`: The [`UserEmail`] to mark as primary
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn set_as_primary(&mut self, user_email: &UserEmail) -> Result<(), Self::Error>; async fn set_as_primary(&mut self, user_email: &UserEmail) -> Result<(), Self::Error>;
/// Add a [`UserEmailVerification`] for a [`UserEmail`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock to use
/// * `user_email`: The [`UserEmail`] for which to add the
/// [`UserEmailVerification`]
/// * `max_age`: The duration for which the [`UserEmailVerification`] is
/// valid
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add_verification_code( async fn add_verification_code(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -61,6 +189,20 @@ pub trait UserEmailRepository: Send + Sync {
code: String, code: String,
) -> Result<UserEmailVerification, Self::Error>; ) -> Result<UserEmailVerification, Self::Error>;
/// Find a [`UserEmailVerification`] for a [`UserEmail`] by its code
///
/// Returns `None` if no matching [`UserEmailVerification`] was found
///
/// # Parameters
///
/// * `clock`: The clock to use
/// * `user_email`: The [`UserEmail`] for which to lookup the
/// [`UserEmailVerification`]
/// * `code`: The code used to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_verification_code( async fn find_verification_code(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
@ -68,6 +210,18 @@ pub trait UserEmailRepository: Send + Sync {
code: &str, code: &str,
) -> Result<Option<UserEmailVerification>, Self::Error>; ) -> Result<Option<UserEmailVerification>, Self::Error>;
/// Consume a [`UserEmailVerification`]
///
/// Returns the consumed [`UserEmailVerification`]
///
/// # Parameters
///
/// * `clock`: The clock to use
/// * `verification`: The [`UserEmailVerification`] to consume
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn consume_verification_code( async fn consume_verification_code(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Repositories to interact with entities related to user accounts
use async_trait::async_trait; use async_trait::async_trait;
use mas_data_model::User; use mas_data_model::User;
use rand_core::RngCore; use rand_core::RngCore;
@ -27,18 +29,70 @@ pub use self::{
email::UserEmailRepository, password::UserPasswordRepository, session::BrowserSessionRepository, email::UserEmailRepository, password::UserPasswordRepository, session::BrowserSessionRepository,
}; };
/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
/// backend
#[async_trait] #[async_trait]
pub trait UserRepository: Send + Sync { pub trait UserRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a [`User`] by its ID
///
/// Returns `None` if no [`User`] was found
///
/// # Parameters
///
/// * `id`: The ID of the [`User`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
/// Find a [`User`] by its username
///
/// Returns `None` if no [`User`] was found
///
/// # Parameters
///
/// * `username`: The username of the [`User`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>; async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
/// Create a new [`User`]
///
/// Returns the newly created [`User`]
///
/// # Parameters
///
/// * `rng`: A random number generator to generate the [`User`] ID
/// * `clock`: The clock used to generate timestamps
/// * `username`: The username of the [`User`]
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
clock: &dyn Clock, clock: &dyn Clock,
username: String, username: String,
) -> Result<User, Self::Error>; ) -> Result<User, Self::Error>;
/// Check if a [`User`] exists
///
/// Returns `true` if the [`User`] exists, `false` otherwise
///
/// # Parameters
///
/// * `username`: The username of the [`User`] to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>; async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
} }

View File

@ -18,11 +18,42 @@ use rand_core::RngCore;
use crate::{repository_impl, Clock}; use crate::{repository_impl, Clock};
/// A [`UserPasswordRepository`] helps interacting with [`Password`] saved in
/// the storage backend
#[async_trait] #[async_trait]
pub trait UserPasswordRepository: Send + Sync { pub trait UserPasswordRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Get the active password for a user
///
/// Returns `None` if the user has no password set
///
/// # Parameters
///
/// * `user`: The user to get the password for
///
/// # Errors
///
/// Returns [`Self::Error`] if underlying repository fails
async fn active(&mut self, user: &User) -> Result<Option<Password>, Self::Error>; async fn active(&mut self, user: &User) -> Result<Option<Password>, Self::Error>;
/// Set a new password for a user
///
/// Returns the newly created [`Password`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `user`: The user to set the password for
/// * `version`: The version of the hashing scheme used
/// * `hashed_password`: The hashed password
/// * `upgraded_from`: The password this password was upgraded from, if any
///
/// # Errors
///
/// Returns [`Self::Error`] if underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),

View File

@ -19,29 +19,105 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination}; use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`]
/// saved in the storage backend
#[async_trait] #[async_trait]
pub trait BrowserSessionRepository: Send + Sync { pub trait BrowserSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error; type Error;
/// Lookup a [`BrowserSession`] by its ID
///
/// Returns `None` if the session is not found
///
/// # Parameters
///
/// * `id`: The ID of the session to lookup
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn lookup(&mut self, id: Ulid) -> Result<Option<BrowserSession>, Self::Error>; async fn lookup(&mut self, id: Ulid) -> Result<Option<BrowserSession>, Self::Error>;
/// Create a new [`BrowserSession`] for a [`User`]
///
/// Returns the newly created [`BrowserSession`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `user`: The user to create the session for
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add( async fn add(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
clock: &dyn Clock, clock: &dyn Clock,
user: &User, user: &User,
) -> Result<BrowserSession, Self::Error>; ) -> Result<BrowserSession, Self::Error>;
/// Finish a [`BrowserSession`]
///
/// Returns the finished session
///
/// # Parameters
///
/// * `clock`: The clock used to generate timestamps
/// * `user_session`: The session to finish
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn finish( async fn finish(
&mut self, &mut self,
clock: &dyn Clock, clock: &dyn Clock,
user_session: BrowserSession, user_session: BrowserSession,
) -> Result<BrowserSession, Self::Error>; ) -> Result<BrowserSession, Self::Error>;
/// List active [`BrowserSession`] for a [`User`] with the given pagination
///
/// # Parameters
///
/// * `user`: The user to list the sessions for
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_active_paginated( async fn list_active_paginated(
&mut self, &mut self,
user: &User, user: &User,
pagination: Pagination, pagination: Pagination,
) -> Result<Page<BrowserSession>, Self::Error>; ) -> Result<Page<BrowserSession>, Self::Error>;
/// Count active [`BrowserSession`] for a [`User`]
///
/// # Parameters
///
/// * `user`: The user to count the sessions for
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn count_active(&mut self, user: &User) -> Result<usize, Self::Error>; async fn count_active(&mut self, user: &User) -> Result<usize, Self::Error>;
/// Authenticate a [`BrowserSession`] with the given [`Password`]
///
/// Returns the updated [`BrowserSession`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `user_session`: The session to authenticate
/// * `user_password`: The password which was used to authenticate
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn authenticate_with_password( async fn authenticate_with_password(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),
@ -50,6 +126,21 @@ pub trait BrowserSessionRepository: Send + Sync {
user_password: &Password, user_password: &Password,
) -> Result<BrowserSession, Self::Error>; ) -> Result<BrowserSession, Self::Error>;
/// Authenticate a [`BrowserSession`] with the given [`UpstreamOAuthLink`]
///
/// Returns the updated [`BrowserSession`]
///
/// # Parameters
///
/// * `rng`: The random number generator to use
/// * `clock`: The clock used to generate timestamps
/// * `user_session`: The session to authenticate
/// * `upstream_oauth_link`: The upstream OAuth link which was used to
/// authenticate
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn authenticate_with_upstream( async fn authenticate_with_upstream(
&mut self, &mut self,
rng: &mut (dyn RngCore + Send), rng: &mut (dyn RngCore + Send),