You've already forked authentication-service
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:
@ -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;
|
||||
|
@ -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};
|
||||
|
@ -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()))
|
||||
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, ¤t_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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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};
|
||||
|
@ -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::{
|
||||
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,
|
||||
expire_compat_access_token, lookup_active_compat_refresh_token,
|
||||
CompatRefreshTokenLookupError,
|
||||
},
|
||||
LookupError,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DurationMilliSeconds};
|
||||
|
@ -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::{
|
||||
use mas_storage::{
|
||||
oauth2::{
|
||||
authorization_grant::new_authorization_grant,
|
||||
client::{lookup_client_by_client_id, ClientFetchError},
|
||||
},
|
||||
LookupError,
|
||||
};
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::{
|
||||
|
@ -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;
|
||||
|
@ -40,7 +40,7 @@ use mas_storage::{
|
||||
RefreshTokenLookupError,
|
||||
},
|
||||
},
|
||||
DatabaseInconsistencyError, PostgresqlBackend,
|
||||
DatabaseInconsistencyError, LookupError, PostgresqlBackend,
|
||||
};
|
||||
use oauth2_types::{
|
||||
errors::{ClientError, ClientErrorCode},
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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: (),
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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(_)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
Reference in New Issue
Block a user