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

GraphQL API

This commit is contained in:
Quentin Gliech
2022-12-02 16:25:23 +01:00
parent 07636dd9e7
commit 2e7112ef13
14 changed files with 645 additions and 223 deletions

View File

@ -22,12 +22,18 @@
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
use async_graphql::{Context, Description, EmptyMutation, EmptySubscription, ID};
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Description, EmptyMutation, EmptySubscription, ID,
};
use mas_axum_utils::SessionInfo;
use mas_storage::LookupResultExt;
use sqlx::PgPool;
use self::model::{BrowserSession, Node, NodeType, OAuth2Client, User, UserEmail};
use self::model::{
BrowserSession, Cursor, Node, NodeCursor, NodeType, OAuth2Client, UpstreamOAuth2Link,
UpstreamOAuth2Provider, User, UserEmail,
};
mod model;
@ -167,6 +173,100 @@ impl RootQuery {
Ok(user_email.map(UserEmail))
}
/// Fetch an upstream OAuth 2.0 link by its ID.
async fn upstream_oauth2_link(
&self,
ctx: &Context<'_>,
id: ID,
) -> Result<Option<UpstreamOAuth2Link>, async_graphql::Error> {
let id = NodeType::UpstreamOAuth2Link.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 link = mas_storage::upstream_oauth2::lookup_link(&mut conn, id)
.await
.to_option()?;
// Ensure that the link belongs to the current user
let link = link.filter(|link| link.user_id == Some(current_user.data));
Ok(link.map(UpstreamOAuth2Link::new))
}
/// Fetch an upstream OAuth 2.0 provider by its ID.
async fn upstream_oauth2_provider(
&self,
ctx: &Context<'_>,
id: ID,
) -> Result<Option<UpstreamOAuth2Provider>, async_graphql::Error> {
let id = NodeType::UpstreamOAuth2Provider.extract_ulid(&id)?;
let database = ctx.data::<PgPool>()?;
let mut conn = database.acquire().await?;
let provider = mas_storage::upstream_oauth2::lookup_provider(&mut conn, id)
.await
.to_option()?;
Ok(provider.map(UpstreamOAuth2Provider::new))
}
/// Get a list of upstream OAuth 2.0 providers.
async fn upstream_oauth2_providers(
&self,
ctx: &Context<'_>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
before: Option<String>,
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
) -> Result<Connection<Cursor, UpstreamOAuth2Provider>, async_graphql::Error> {
let database = ctx.data::<PgPool>()?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let mut conn = database.acquire().await?;
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| {
x.extract_for_type(NodeType::UpstreamOAuth2Provider)
})
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| {
x.extract_for_type(NodeType::UpstreamOAuth2Provider)
})
.transpose()?;
let (has_previous_page, has_next_page, edges) =
mas_storage::upstream_oauth2::get_paginated_providers(
&mut conn, before_id, after_id, first, last,
)
.await?;
let mut connection = Connection::new(has_previous_page, has_next_page);
connection.edges.extend(edges.into_iter().map(|p| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, p.id)),
UpstreamOAuth2Provider::new(p),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// 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)?;
@ -178,6 +278,16 @@ impl RootQuery {
| NodeType::CompatSsoLogin
| NodeType::OAuth2Session => None,
NodeType::UpstreamOAuth2Provider => self
.upstream_oauth2_provider(ctx, id)
.await?
.map(|c| Node::UpstreamOAuth2Provider(Box::new(c))),
NodeType::UpstreamOAuth2Link => self
.upstream_oauth2_link(ctx, id)
.await?
.map(|c| Node::UpstreamOAuth2Link(Box::new(c))),
NodeType::OAuth2Client => self
.oauth2_client(ctx, id)
.await?

View File

@ -20,6 +20,7 @@ mod compat_sessions;
mod cursor;
mod node;
mod oauth;
mod upstream_oauth;
mod users;
pub use self::{
@ -28,6 +29,7 @@ pub use self::{
cursor::{Cursor, NodeCursor},
node::{Node, NodeType},
oauth::{OAuth2Client, OAuth2Consent, OAuth2Session},
upstream_oauth::{UpstreamOAuth2Link, UpstreamOAuth2Provider},
users::{User, UserEmail},
};
@ -42,4 +44,6 @@ pub enum CreationEvent {
CompatSession(Box<CompatSession>),
BrowserSession(Box<BrowserSession>),
UserEmail(Box<UserEmail>),
UpstreamOAuth2Provider(Box<UpstreamOAuth2Provider>),
UpstreamOAuth2Link(Box<UpstreamOAuth2Link>),
}

View File

@ -19,7 +19,7 @@ use ulid::Ulid;
use super::{
Authentication, BrowserSession, CompatSession, CompatSsoLogin, OAuth2Client, OAuth2Session,
User, UserEmail,
UpstreamOAuth2Link, UpstreamOAuth2Provider, User, UserEmail,
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -30,6 +30,8 @@ pub enum NodeType {
CompatSsoLogin,
OAuth2Client,
OAuth2Session,
UpstreamOAuth2Provider,
UpstreamOAuth2Link,
User,
UserEmail,
}
@ -52,6 +54,8 @@ impl NodeType {
NodeType::CompatSsoLogin => "compat_sso_login",
NodeType::OAuth2Client => "oauth2_client",
NodeType::OAuth2Session => "oauth2_session",
NodeType::UpstreamOAuth2Provider => "upstream_oauth2_provider",
NodeType::UpstreamOAuth2Link => "upstream_oauth2_link",
NodeType::User => "user",
NodeType::UserEmail => "user_email",
}
@ -65,6 +69,8 @@ impl NodeType {
"compat_sso_login" => Some(NodeType::CompatSsoLogin),
"oauth2_client" => Some(NodeType::OAuth2Client),
"oauth2_session" => Some(NodeType::OAuth2Session),
"upstream_oauth2_provider" => Some(NodeType::UpstreamOAuth2Provider),
"upstream_oauth2_link" => Some(NodeType::UpstreamOAuth2Link),
"user" => Some(NodeType::User),
"user_email" => Some(NodeType::UserEmail),
_ => None,
@ -116,6 +122,8 @@ pub enum Node {
CompatSsoLogin(Box<CompatSsoLogin>),
OAuth2Client(Box<OAuth2Client>),
OAuth2Session(Box<OAuth2Session>),
UpstreamOAuth2Provider(Box<UpstreamOAuth2Provider>),
UpstreamOAuth2Link(Box<UpstreamOAuth2Link>),
User(Box<User>),
UserEmail(Box<UserEmail>),
}

View File

@ -0,0 +1,121 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{Context, Object, ID};
use chrono::{DateTime, Utc};
use mas_storage::PostgresqlBackend;
use sqlx::PgPool;
use super::{NodeType, User};
#[derive(Debug, Clone)]
pub struct UpstreamOAuth2Provider {
provider: mas_data_model::UpstreamOAuthProvider,
}
impl UpstreamOAuth2Provider {
#[must_use]
pub const fn new(provider: mas_data_model::UpstreamOAuthProvider) -> Self {
Self { provider }
}
}
#[Object]
impl UpstreamOAuth2Provider {
/// ID of the object.
pub async fn id(&self) -> ID {
NodeType::UpstreamOAuth2Provider.id(self.provider.id)
}
/// When the object was created.
pub async fn created_at(&self) -> DateTime<Utc> {
self.provider.created_at
}
/// OpenID Connect issuer URL.
pub async fn issuer(&self) -> &str {
&self.provider.issuer
}
/// Client ID used for this provider.
pub async fn client_id(&self) -> &str {
&self.provider.client_id
}
}
impl UpstreamOAuth2Link {
#[must_use]
pub const fn new(link: mas_data_model::UpstreamOAuthLink) -> Self {
Self {
link,
provider: None,
user: None,
}
}
}
#[derive(Debug, Clone)]
pub struct UpstreamOAuth2Link {
link: mas_data_model::UpstreamOAuthLink,
provider: Option<mas_data_model::UpstreamOAuthProvider>,
user: Option<mas_data_model::User<PostgresqlBackend>>,
}
#[Object]
impl UpstreamOAuth2Link {
/// ID of the object.
pub async fn id(&self) -> ID {
NodeType::UpstreamOAuth2Link.id(self.link.id)
}
/// When the object was created.
pub async fn created_at(&self) -> DateTime<Utc> {
self.link.created_at
}
/// The provider for which this link is.
pub async fn provider(
&self,
ctx: &Context<'_>,
) -> Result<UpstreamOAuth2Provider, async_graphql::Error> {
let provider = if let Some(provider) = &self.provider {
// Cached
provider.clone()
} else {
// Fetch on-the-fly
let database = ctx.data::<PgPool>()?;
let mut conn = database.acquire().await?;
mas_storage::upstream_oauth2::lookup_provider(&mut conn, self.link.provider_id).await?
};
Ok(UpstreamOAuth2Provider::new(provider))
}
/// The user to which this link is associated.
pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
let user = if let Some(user) = &self.user {
// Cached
user.clone()
} else if let Some(user_id) = &self.link.user_id {
// Fetch on-the-fly
let database = ctx.data::<PgPool>()?;
let mut conn = database.acquire().await?;
mas_storage::user::lookup_user(&mut conn, *user_id).await?
} else {
return Ok(None);
};
Ok(Some(User(user)))
}
}