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

@ -20,20 +20,58 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// A [`CompatAccessTokenRepository`] helps interacting with
/// [`CompatAccessToken`] saved in the storage backend
#[async_trait]
pub trait CompatAccessTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
access_token: &str,
) -> Result<Option<CompatAccessToken>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -44,6 +82,13 @@ pub trait CompatAccessTokenRepository: Send + Sync {
) -> Result<CompatAccessToken, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,

View File

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

View File

@ -19,20 +19,55 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// A [`CompatRefreshTokenRepository`] helps interacting with
/// [`CompatRefreshToken`] saved in the storage backend
#[async_trait]
pub trait CompatRefreshTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
refresh_token: &str,
) -> Result<Option<CompatRefreshToken>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -43,6 +78,17 @@ pub trait CompatRefreshTokenRepository: Send + Sync {
) -> Result<CompatRefreshToken, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,

View File

@ -19,14 +19,40 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// A [`CompatSessionRepository`] helps interacting with
/// [`CompatSessionRepository`] saved in the storage backend
#[async_trait]
pub trait CompatSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -36,6 +62,17 @@ pub trait CompatSessionRepository: Send + Sync {
) -> Result<CompatSession, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,

View File

@ -20,20 +20,56 @@ use url::Url;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`CompatSsoLoginRepository`] helps interacting with
/// [`CompatSsoLoginRepository`] saved in the storage backend
#[async_trait]
pub trait CompatSsoLoginRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
login_token: &str,
) -> Result<Option<CompatSsoLogin>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -43,6 +79,19 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,
@ -51,6 +100,17 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,
@ -58,6 +118,15 @@ pub trait CompatSsoLoginRepository: Send + Sync {
) -> Result<CompatSsoLogin, Self::Error>;
/// 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(
&mut self,
user: &User,

View File

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

View File

@ -20,20 +20,57 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// An [`OAuth2AccessTokenRepository`] helps interacting with [`AccessToken`]
/// saved in the storage backend
#[async_trait]
pub trait OAuth2AccessTokenRepository: 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<AccessToken>, 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<AccessToken>, 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
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn add(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -44,6 +81,17 @@ pub trait OAuth2AccessTokenRepository: Send + Sync {
) -> Result<AccessToken, 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,
@ -51,6 +99,16 @@ pub trait OAuth2AccessTokenRepository: Send + Sync {
) -> Result<AccessToken, Self::Error>;
/// 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>;
}

View File

@ -23,10 +23,38 @@ use url::Url;
use crate::{repository_impl, Clock};
/// An [`OAuth2AuthorizationGrantRepository`] helps interacting with
/// [`AuthorizationGrant`] saved in the storage backend
#[async_trait]
pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
/// The error type returned by the repository
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)]
async fn add(
&mut self,
@ -44,11 +72,47 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
requires_consent: bool,
) -> 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>;
/// 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)
-> 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(
&mut self,
clock: &dyn Clock,
@ -56,12 +120,35 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
authorization_grant: AuthorizationGrant,
) -> 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(
&mut self,
clock: &dyn Clock,
authorization_grant: AuthorizationGrant,
) -> 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(
&mut self,
authorization_grant: AuthorizationGrant,

View File

@ -25,22 +25,82 @@ use url::Url;
use crate::{repository_impl, Clock};
/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the
/// storage backend
#[async_trait]
pub trait OAuth2ClientRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// Find an OAuth2 client by its client ID
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) };
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(
&mut self,
ids: BTreeSet<Ulid>,
) -> 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)]
async fn add(
&mut self,
@ -64,6 +124,24 @@ pub trait OAuth2ClientRepository: Send + Sync {
initiate_login_uri: Option<Url>,
) -> 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)]
async fn add_from_config(
&mut self,
@ -77,12 +155,36 @@ pub trait OAuth2ClientRepository: Send + Sync {
redirect_uris: Vec<Url>,
) -> 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(
&mut self,
client: &Client,
user: &User,
) -> 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(
&mut self,
rng: &mut (dyn RngCore + Send),

View File

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

View File

@ -19,20 +19,58 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// An [`OAuth2RefreshTokenRepository`] helps interacting with [`RefreshToken`]
/// saved in the storage backend
#[async_trait]
pub trait OAuth2RefreshTokenRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
refresh_token: &str,
) -> Result<Option<RefreshToken>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -43,6 +81,18 @@ pub trait OAuth2RefreshTokenRepository: Send + Sync {
) -> Result<RefreshToken, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,

View File

@ -19,12 +19,41 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`OAuth2SessionRepository`] helps interacting with [`Session`]
/// saved in the storage backend
#[async_trait]
pub trait OAuth2SessionRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -33,9 +62,31 @@ pub trait OAuth2SessionRepository: Send + Sync {
user_session: &BrowserSession,
) -> 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)
-> 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(
&mut self,
user: &User,

View File

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

View File

@ -19,14 +19,39 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`UpstreamOAuthLinkRepository`] helps interacting with
/// [`UpstreamOAuthLink`] with the storage backend
#[async_trait]
pub trait UpstreamOAuthLinkRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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(
&mut self,
upstream_oauth_provider: &UpstreamOAuthProvider,
@ -34,6 +59,20 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -43,6 +82,17 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<UpstreamOAuthLink, Self::Error>;
/// 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(
&mut self,
upstream_oauth_link: &UpstreamOAuthLink,
@ -50,6 +100,15 @@ pub trait UpstreamOAuthLinkRepository: Send + Sync {
) -> Result<(), Self::Error>;
/// 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(
&mut self,
user: &User,

View File

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

View File

@ -21,14 +21,47 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// An [`UpstreamOAuthProviderRepository`] helps interacting with
/// [`UpstreamOAuthProvider`] saved in the storage backend
#[async_trait]
pub trait UpstreamOAuthProviderRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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>;
/// 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)]
async fn add(
&mut self,
@ -43,12 +76,24 @@ pub trait UpstreamOAuthProviderRepository: Send + Sync {
) -> Result<UpstreamOAuthProvider, Self::Error>;
/// 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(
&mut self,
pagination: Pagination,
) -> Result<Page<UpstreamOAuthProvider>, Self::Error>;
/// Get all upstream OAuth providers
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn all(&mut self) -> Result<Vec<UpstreamOAuthProvider>, Self::Error>;
}

View File

@ -19,17 +19,48 @@ use ulid::Ulid;
use crate::{repository_impl, Clock};
/// An [`UpstreamOAuthSessionRepository`] helps interacting with
/// [`UpstreamOAuthAuthorizationSession`] saved in the storage backend
#[async_trait]
pub trait UpstreamOAuthSessionRepository: Send + Sync {
/// The error type returned by the repository
type Error;
/// 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(
&mut self,
id: Ulid,
) -> Result<Option<UpstreamOAuthAuthorizationSession>, Self::Error>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -41,6 +72,20 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,
@ -50,6 +95,17 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
/// 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(
&mut self,
clock: &dyn Clock,

View File

@ -19,22 +19,105 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`UserEmailRepository`] helps interacting with [`UserEmail`] saved in the
/// storage backend
#[async_trait]
pub trait UserEmailRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// 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>;
/// 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>;
/// 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>;
/// 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(
&mut self,
user: &User,
pagination: Pagination,
) -> 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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -42,16 +125,61 @@ pub trait UserEmailRepository: Send + Sync {
user: &User,
email: String,
) -> 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>;
/// 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(
&mut self,
clock: &dyn Clock,
user_email: UserEmail,
) -> 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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -61,6 +189,20 @@ pub trait UserEmailRepository: Send + Sync {
code: String,
) -> 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(
&mut self,
clock: &dyn Clock,
@ -68,6 +210,18 @@ pub trait UserEmailRepository: Send + Sync {
code: &str,
) -> 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(
&mut self,
clock: &dyn Clock,

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Repositories to interact with entities related to user accounts
use async_trait::async_trait;
use mas_data_model::User;
use rand_core::RngCore;
@ -27,18 +29,70 @@ pub use self::{
email::UserEmailRepository, password::UserPasswordRepository, session::BrowserSessionRepository,
};
/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
/// backend
#[async_trait]
pub trait UserRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// 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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
username: String,
) -> 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>;
}

View File

@ -18,11 +18,42 @@ use rand_core::RngCore;
use crate::{repository_impl, Clock};
/// A [`UserPasswordRepository`] helps interacting with [`Password`] saved in
/// the storage backend
#[async_trait]
pub trait UserPasswordRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),

View File

@ -19,29 +19,105 @@ use ulid::Ulid;
use crate::{pagination::Page, repository_impl, Clock, Pagination};
/// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`]
/// saved in the storage backend
#[async_trait]
pub trait BrowserSessionRepository: Send + Sync {
/// The error type returned by the repository
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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
clock: &dyn Clock,
user: &User,
) -> 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(
&mut self,
clock: &dyn Clock,
user_session: BrowserSession,
) -> 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(
&mut self,
user: &User,
pagination: Pagination,
) -> 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>;
/// 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(
&mut self,
rng: &mut (dyn RngCore + Send),
@ -50,6 +126,21 @@ pub trait BrowserSessionRepository: Send + Sync {
user_password: &Password,
) -> 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(
&mut self,
rng: &mut (dyn RngCore + Send),