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

Allow fetching more nodes by their IDs

This commit is contained in:
Quentin Gliech
2022-11-16 17:21:40 +01:00
parent 10815d8101
commit 78778648ca
18 changed files with 232 additions and 67 deletions

View File

@ -29,7 +29,7 @@ use http::{header::WWW_AUTHENTICATE, HeaderMap, HeaderValue, Request, StatusCode
use mas_data_model::Session;
use mas_storage::{
oauth2::access_token::{lookup_active_access_token, AccessTokenLookupError},
PostgresqlBackend,
LookupError, PostgresqlBackend,
};
use serde::{de::DeserializeOwned, Deserialize};
use sqlx::PgConnection;

View File

@ -20,7 +20,7 @@ use mas_storage::{
user::{
lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user,
},
Clock,
Clock, LookupError,
};
use rand::SeedableRng;
use tracing::{info, warn};

View File

@ -24,10 +24,10 @@
use async_graphql::{Context, Description, EmptyMutation, EmptySubscription, ID};
use mas_axum_utils::SessionInfo;
use model::NodeType;
use mas_storage::LookupResultExt;
use sqlx::PgPool;
use self::model::{BrowserSession, Node, User};
use self::model::{BrowserSession, Node, NodeType, OAuth2Client, User, UserEmail};
mod model;
@ -80,33 +80,122 @@ impl RootQuery {
Ok(session.map(User::from))
}
/// Fetches an object given its ID.
async fn node(&self, ctx: &Context<'_>, id: ID) -> Result<Option<Node>, async_graphql::Error> {
let (node_type, id) = NodeType::from_id(&id)?;
/// Fetch an OAuth 2.0 client by its ID.
async fn oauth2_client(
&self,
ctx: &Context<'_>,
id: ID,
) -> Result<Option<OAuth2Client>, async_graphql::Error> {
let id = NodeType::OAuth2Client.extract_ulid(&id)?;
let database = ctx.data::<PgPool>()?;
let mut conn = database.acquire().await?;
let client = mas_storage::oauth2::client::lookup_client(&mut conn, id)
.await
.to_option()?;
Ok(client.map(OAuth2Client))
}
/// Fetch a user by its ID.
async fn user(&self, ctx: &Context<'_>, id: ID) -> Result<Option<User>, async_graphql::Error> {
let id = NodeType::User.extract_ulid(&id)?;
let database = ctx.data::<PgPool>()?;
let session_info = ctx.data::<SessionInfo>()?;
let mut conn = database.acquire().await?;
let session = session_info.load_session(&mut conn).await?;
let Some(session) = session else { return Ok(None) };
let current_user = session.user;
match node_type {
// TODO
NodeType::Authentication
| NodeType::BrowserSession
| NodeType::CompatSession
| NodeType::CompatSsoLogin
| NodeType::OAuth2Client
| NodeType::UserEmail
| NodeType::OAuth2Session => Ok(None),
NodeType::User => {
if session.user.data == id {
Ok(Some(Box::new(User(session.user)).into()))
} else {
Ok(None)
}
}
if current_user.data == id {
Ok(Some(User(current_user)))
} else {
Ok(None)
}
}
/// Fetch a browser session by its ID.
async fn browser_session(
&self,
ctx: &Context<'_>,
id: ID,
) -> Result<Option<BrowserSession>, async_graphql::Error> {
let id = NodeType::BrowserSession.extract_ulid(&id)?;
let database = ctx.data::<PgPool>()?;
let session_info = ctx.data::<SessionInfo>()?;
let mut conn = database.acquire().await?;
let session = session_info.load_session(&mut conn).await?;
let Some(session) = session else { return Ok(None) };
let current_user = session.user;
let browser_session = mas_storage::user::lookup_active_session(&mut conn, id)
.await
.to_option()?;
let ret = browser_session.and_then(|browser_session| {
if browser_session.user.data == current_user.data {
Some(BrowserSession(browser_session))
} else {
None
}
});
Ok(ret)
}
/// Fetch a user email by its ID.
async fn user_email(
&self,
ctx: &Context<'_>,
id: ID,
) -> Result<Option<UserEmail>, async_graphql::Error> {
let id = NodeType::UserEmail.extract_ulid(&id)?;
let database = ctx.data::<PgPool>()?;
let session_info = ctx.data::<SessionInfo>()?;
let mut conn = database.acquire().await?;
let session = session_info.load_session(&mut conn).await?;
let Some(session) = session else { return Ok(None) };
let current_user = session.user;
let user_email = mas_storage::user::lookup_user_email_by_id(&mut conn, &current_user, id)
.await
.to_option()?;
Ok(user_email.map(UserEmail))
}
/// Fetches an object given its ID.
async fn node(&self, ctx: &Context<'_>, id: ID) -> Result<Option<Node>, async_graphql::Error> {
let (node_type, _id) = NodeType::from_id(&id)?;
let ret = match node_type {
// TODO
NodeType::Authentication
| NodeType::CompatSession
| NodeType::CompatSsoLogin
| NodeType::OAuth2Session => None,
NodeType::OAuth2Client => self
.oauth2_client(ctx, id)
.await?
.map(|c| Node::OAuth2Client(Box::new(c))),
NodeType::UserEmail => self
.user_email(ctx, id)
.await?
.map(|e| Node::UserEmail(Box::new(e))),
NodeType::BrowserSession => self
.browser_session(ctx, id)
.await?
.map(|s| Node::BrowserSession(Box::new(s))),
NodeType::User => self.user(ctx, id).await?.map(|u| Node::User(Box::new(u))),
};
Ok(ret)
}
}

View File

@ -40,6 +40,7 @@ pub enum InvalidID {
InvalidFormat,
InvalidUlid(#[from] ulid::DecodeError),
UnknownPrefix,
TypeMismatch { got: NodeType, expected: NodeType },
}
impl NodeType {
@ -90,6 +91,19 @@ impl NodeType {
pub fn from_id(id: &ID) -> Result<(Self, Ulid), InvalidID> {
Self::deserialize(&id.0)
}
pub fn extract_ulid(self, id: &ID) -> Result<Ulid, InvalidID> {
let (node_type, ulid) = Self::deserialize(&id.0)?;
if node_type == self {
Ok(ulid)
} else {
Err(InvalidID::TypeMismatch {
got: node_type,
expected: self,
})
}
}
}
/// An object with an ID.

View File

@ -256,7 +256,7 @@ impl User {
/// A user email address
#[derive(Description)]
pub struct UserEmail(mas_data_model::UserEmail<PostgresqlBackend>);
pub struct UserEmail(pub mas_data_model::UserEmail<PostgresqlBackend>);
#[Object(use_type_description)]
impl UserEmail {

View File

@ -22,7 +22,7 @@ use mas_storage::{
get_compat_sso_login_by_token, mark_compat_sso_login_as_exchanged,
CompatSsoLoginLookupError,
},
Clock, PostgresqlBackend,
Clock, LookupError, PostgresqlBackend,
};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds};

View File

@ -16,9 +16,13 @@ use axum::{extract::State, response::IntoResponse, Json};
use chrono::Duration;
use hyper::StatusCode;
use mas_data_model::{TokenFormatError, TokenType};
use mas_storage::compat::{
add_compat_access_token, add_compat_refresh_token, consume_compat_refresh_token,
expire_compat_access_token, lookup_active_compat_refresh_token, CompatRefreshTokenLookupError,
use mas_storage::{
compat::{
add_compat_access_token, add_compat_refresh_token, consume_compat_refresh_token,
expire_compat_access_token, lookup_active_compat_refresh_token,
CompatRefreshTokenLookupError,
},
LookupError,
};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DurationMilliSeconds};

View File

@ -26,9 +26,12 @@ use mas_data_model::{AuthorizationCode, Pkce};
use mas_keystore::Encrypter;
use mas_policy::PolicyFactory;
use mas_router::{PostAuthAction, Route};
use mas_storage::oauth2::{
authorization_grant::new_authorization_grant,
client::{lookup_client_by_client_id, ClientFetchError},
use mas_storage::{
oauth2::{
authorization_grant::new_authorization_grant,
client::{lookup_client_by_client_id, ClientFetchError},
},
LookupError,
};
use mas_templates::Templates;
use oauth2_types::{

View File

@ -28,7 +28,7 @@ use mas_storage::{
client::ClientFetchError,
refresh_token::{lookup_active_refresh_token, RefreshTokenLookupError},
},
Clock,
Clock, LookupError,
};
use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse};
use sqlx::PgPool;

View File

@ -40,7 +40,7 @@ use mas_storage::{
RefreshTokenLookupError,
},
},
DatabaseInconsistencyError, PostgresqlBackend,
DatabaseInconsistencyError, LookupError, PostgresqlBackend,
};
use oauth2_types::{
errors::{ClientError, ClientErrorCode},

View File

@ -31,7 +31,7 @@ use uuid::Uuid;
use crate::{
pagination::{process_page, QueryBuilderExt},
user::lookup_user_by_username,
Clock, DatabaseInconsistencyError, PostgresqlBackend,
Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend,
};
struct CompatAccessTokenLookup {
@ -59,9 +59,8 @@ pub enum CompatAccessTokenLookupError {
Inconsistency(#[from] DatabaseInconsistencyError),
}
impl CompatAccessTokenLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for CompatAccessTokenLookupError {
fn not_found(&self) -> bool {
matches!(
self,
Self::Database(sqlx::Error::RowNotFound) | Self::Expired { .. }
@ -194,9 +193,8 @@ pub enum CompatRefreshTokenLookupError {
Inconsistency(#[from] DatabaseInconsistencyError),
}
impl CompatRefreshTokenLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for CompatRefreshTokenLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Database(sqlx::Error::RowNotFound))
}
}
@ -752,9 +750,8 @@ pub enum CompatSsoLoginLookupError {
Inconsistency(#[from] DatabaseInconsistencyError),
}
impl CompatSsoLoginLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for CompatSsoLoginLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Database(sqlx::Error::RowNotFound))
}
}

View File

@ -35,6 +35,54 @@ use sqlx::migrate::Migrator;
use thiserror::Error;
use ulid::Ulid;
#[derive(Debug, Error)]
#[error("failed to lookup {what}")]
pub struct GenericLookupError {
what: &'static str,
source: sqlx::Error,
}
impl GenericLookupError {
#[must_use]
pub fn what(what: &'static str) -> Box<dyn Fn(sqlx::Error) -> Self> {
Box::new(move |source: sqlx::Error| Self { what, source })
}
}
impl LookupError for GenericLookupError {
fn not_found(&self) -> bool {
matches!(self.source, sqlx::Error::RowNotFound)
}
}
pub trait LookupError {
fn not_found(&self) -> bool;
}
pub trait LookupResultExt {
type Error;
type Output;
/// Transform a [`Result`] with a [`LookupError`] to transform "not
/// found" errors into [`None`]
fn to_option(self) -> Result<Option<Self::Output>, Self::Error>;
}
impl<T, E> LookupResultExt for Result<T, E>
where
E: LookupError,
{
type Output = T;
type Error = E;
fn to_option(self) -> Result<Option<Self::Output>, Self::Error> {
match self {
Ok(v) => Ok(Some(v)),
Err(e) if e.not_found() => Ok(None),
Err(e) => Err(e),
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct Clock {
_private: (),

View File

@ -22,7 +22,7 @@ use ulid::Ulid;
use uuid::Uuid;
use super::client::{lookup_client, ClientFetchError};
use crate::{Clock, DatabaseInconsistencyError, PostgresqlBackend};
use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend};
#[tracing::instrument(
skip_all,
@ -103,9 +103,8 @@ pub enum AccessTokenLookupError {
Inconsistency(#[from] DatabaseInconsistencyError),
}
impl AccessTokenLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for AccessTokenLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Database(sqlx::Error::RowNotFound))
}
}

View File

@ -28,7 +28,7 @@ use ulid::Ulid;
use url::Url;
use uuid::Uuid;
use crate::{Clock, PostgresqlBackend};
use crate::{Clock, LookupError, PostgresqlBackend};
// XXX: response_types & contacts
#[derive(Debug)]
@ -81,9 +81,8 @@ pub enum ClientFetchError {
Database(#[from] sqlx::Error),
}
impl ClientFetchError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for ClientFetchError {
fn not_found(&self) -> bool {
matches!(
self,
Self::Database(sqlx::Error::RowNotFound) | Self::InvalidClientId(_)

View File

@ -24,7 +24,7 @@ use ulid::Ulid;
use uuid::Uuid;
use super::client::{lookup_client, ClientFetchError};
use crate::{Clock, DatabaseInconsistencyError, PostgresqlBackend};
use crate::{Clock, DatabaseInconsistencyError, LookupError, PostgresqlBackend};
#[tracing::instrument(
skip_all,
@ -106,9 +106,8 @@ pub enum RefreshTokenLookupError {
Conversion(#[from] DatabaseInconsistencyError),
}
impl RefreshTokenLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for RefreshTokenLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Fetch(sqlx::Error::RowNotFound))
}
}

View File

@ -33,7 +33,7 @@ use uuid::Uuid;
use super::{DatabaseInconsistencyError, PostgresqlBackend};
use crate::{
pagination::{process_page, QueryBuilderExt},
Clock,
Clock, GenericLookupError, LookupError,
};
#[derive(Debug, Clone)]
@ -117,9 +117,8 @@ pub enum ActiveSessionLookupError {
Conversion(#[from] DatabaseInconsistencyError),
}
impl ActiveSessionLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for ActiveSessionLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Fetch(sqlx::Error::RowNotFound))
}
}
@ -566,9 +565,8 @@ pub enum UserLookupError {
Inconsistency(#[from] DatabaseInconsistencyError),
}
impl UserLookupError {
#[must_use]
pub fn not_found(&self) -> bool {
impl LookupError for UserLookupError {
fn not_found(&self) -> bool {
matches!(self, Self::Database(sqlx::Error::RowNotFound))
}
}
@ -955,13 +953,13 @@ pub async fn lookup_user_email(
user.id = %user.data,
user_email.id = %id,
),
err(Display),
err,
)]
pub async fn lookup_user_email_by_id(
executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>,
id: Ulid,
) -> Result<UserEmail<PostgresqlBackend>, anyhow::Error> {
) -> Result<UserEmail<PostgresqlBackend>, GenericLookupError> {
let res = sqlx::query_as!(
UserEmailLookup,
r#"
@ -981,7 +979,7 @@ pub async fn lookup_user_email_by_id(
.fetch_one(executor)
.instrument(info_span!("Lookup user email"))
.await
.context("could not lookup user email")?;
.map_err(GenericLookupError::what("user email"))?;
Ok(res.into())
}

View File

@ -294,6 +294,22 @@ type RootQuery {
"""
currentUser: User
"""
Fetch an OAuth 2.0 client by its ID.
"""
oauth2Client(id: ID!): Oauth2Client
"""
Fetch a user by its ID.
"""
user(id: ID!): User
"""
Fetch a browser session by its ID.
"""
browserSession(id: ID!): BrowserSession
"""
Fetch a user email by its ID.
"""
userEmail(id: ID!): UserEmail
"""
Fetches an object given its ID.
"""
node(id: ID!): Node

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { graphql, usePaginationFragment } from "react-relay";
import Block from "./Block";
import BlockList from "./BlockList";
import Button from "./Button";
import OAuth2Session from "./OAuth2Session";