You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
GraphQL API: query oauth2 sessions and clients
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2602,6 +2602,7 @@ dependencies = [
|
|||||||
"mas-axum-utils",
|
"mas-axum-utils",
|
||||||
"mas-data-model",
|
"mas-data-model",
|
||||||
"mas-storage",
|
"mas-storage",
|
||||||
|
"oauth2-types",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -14,6 +14,7 @@ tokio = { version = "1.21.2", features = ["time"] }
|
|||||||
ulid = "1.0.0"
|
ulid = "1.0.0"
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
|
||||||
|
oauth2-types = { path = "../oauth2-types" }
|
||||||
mas-axum-utils = { path = "../axum-utils" }
|
mas-axum-utils = { path = "../axum-utils" }
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
mas-storage = { path = "../storage" }
|
mas-storage = { path = "../storage" }
|
||||||
|
@ -22,6 +22,7 @@ pub enum NodeType {
|
|||||||
UserEmail,
|
UserEmail,
|
||||||
BrowserSession,
|
BrowserSession,
|
||||||
CompatSsoLogin,
|
CompatSsoLogin,
|
||||||
|
OAuth2Session,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
mod browser_sessions;
|
mod browser_sessions;
|
||||||
mod compat_sessions;
|
mod compat_sessions;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod oauth;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
browser_sessions::{Authentication, BrowserSession},
|
browser_sessions::{Authentication, BrowserSession},
|
||||||
cursor::{Cursor, NodeCursor, NodeType},
|
cursor::{Cursor, NodeCursor, NodeType},
|
||||||
|
oauth::{OAuth2Client, OAuth2Consent, OAuth2Session},
|
||||||
users::{User, UserEmail},
|
users::{User, UserEmail},
|
||||||
};
|
};
|
98
crates/graphql/src/model/oauth.rs
Normal file
98
crates/graphql/src/model/oauth.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// 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 mas_storage::{oauth2::client::lookup_client, PostgresqlBackend};
|
||||||
|
use oauth2_types::scope::Scope;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use ulid::Ulid;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::{BrowserSession, User};
|
||||||
|
|
||||||
|
pub struct OAuth2Session(pub mas_data_model::Session<PostgresqlBackend>);
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl OAuth2Session {
|
||||||
|
pub async fn id(&self) -> ID {
|
||||||
|
ID(self.0.data.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client(&self) -> OAuth2Client {
|
||||||
|
OAuth2Client(self.0.client.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn scope(&self) -> String {
|
||||||
|
self.0.scope.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn browser_session(&self) -> BrowserSession {
|
||||||
|
BrowserSession(self.0.browser_session.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user(&self) -> User {
|
||||||
|
User(self.0.browser_session.user.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2Client(pub mas_data_model::Client<PostgresqlBackend>);
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl OAuth2Client {
|
||||||
|
pub async fn id(&self) -> ID {
|
||||||
|
ID(self.0.data.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_id(&self) -> &str {
|
||||||
|
&self.0.client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_name(&self) -> Option<&str> {
|
||||||
|
self.0.client_name.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_uri(&self) -> Option<&Url> {
|
||||||
|
self.0.client_uri.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn tos_uri(&self) -> Option<&Url> {
|
||||||
|
self.0.tos_uri.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn policy_uri(&self) -> Option<&Url> {
|
||||||
|
self.0.policy_uri.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn redirect_uris(&self) -> &[Url] {
|
||||||
|
&self.0.redirect_uris
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2Consent {
|
||||||
|
scope: Scope,
|
||||||
|
client_id: Ulid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl OAuth2Consent {
|
||||||
|
pub async fn scope(&self) -> String {
|
||||||
|
self.scope.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
|
||||||
|
let mut conn = ctx.data::<PgPool>()?.acquire().await?;
|
||||||
|
let client = lookup_client(&mut conn, self.client_id).await?;
|
||||||
|
Ok(OAuth2Client(client))
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,9 @@ use chrono::{DateTime, Utc};
|
|||||||
use mas_storage::PostgresqlBackend;
|
use mas_storage::PostgresqlBackend;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use super::{compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType};
|
use super::{
|
||||||
|
compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType, OAuth2Session,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct User(pub mas_data_model::User<PostgresqlBackend>);
|
pub struct User(pub mas_data_model::User<PostgresqlBackend>);
|
||||||
|
|
||||||
@ -185,6 +187,50 @@ impl User {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn oauth2_sessions(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i32>,
|
||||||
|
last: Option<i32>,
|
||||||
|
) -> Result<Connection<Cursor, OAuth2Session>, 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::OAuth2Session))
|
||||||
|
.transpose()?;
|
||||||
|
let before_id = before
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, edges) =
|
||||||
|
mas_storage::oauth2::get_paginated_user_oauth_sessions(
|
||||||
|
&mut conn, &self.0, 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(|s| {
|
||||||
|
Edge::new(
|
||||||
|
OpaqueCursor(NodeCursor(NodeType::OAuth2Session, s.data)),
|
||||||
|
OAuth2Session(s),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok::<_, async_graphql::Error>(connection)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserEmail(mas_data_model::UserEmail<PostgresqlBackend>);
|
pub struct UserEmail(mas_data_model::UserEmail<PostgresqlBackend>);
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
use std::string::ToString;
|
use std::{collections::HashMap, string::ToString};
|
||||||
|
|
||||||
use mas_data_model::{Client, JwksOrJwksUri};
|
use mas_data_model::{Client, JwksOrJwksUri};
|
||||||
use mas_iana::{
|
use mas_iana::{
|
||||||
@ -250,6 +250,54 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
pub async fn lookup_clients(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
ids: impl IntoIterator<Item = Ulid> + Send,
|
||||||
|
) -> Result<HashMap<Ulid, Client<PostgresqlBackend>>, ClientFetchError> {
|
||||||
|
let ids: Vec<Uuid> = ids.into_iter().map(Uuid::from).collect();
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
OAuth2ClientLookup,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
c.oauth2_client_id,
|
||||||
|
c.encrypted_client_secret,
|
||||||
|
ARRAY(
|
||||||
|
SELECT redirect_uri
|
||||||
|
FROM oauth2_client_redirect_uris r
|
||||||
|
WHERE r.oauth2_client_id = c.oauth2_client_id
|
||||||
|
) AS "redirect_uris!",
|
||||||
|
c.grant_type_authorization_code,
|
||||||
|
c.grant_type_refresh_token,
|
||||||
|
c.client_name,
|
||||||
|
c.logo_uri,
|
||||||
|
c.client_uri,
|
||||||
|
c.policy_uri,
|
||||||
|
c.tos_uri,
|
||||||
|
c.jwks_uri,
|
||||||
|
c.jwks,
|
||||||
|
c.id_token_signed_response_alg,
|
||||||
|
c.userinfo_signed_response_alg,
|
||||||
|
c.token_endpoint_auth_method,
|
||||||
|
c.token_endpoint_auth_signing_alg,
|
||||||
|
c.initiate_login_uri
|
||||||
|
FROM oauth2_clients c
|
||||||
|
|
||||||
|
WHERE c.oauth2_client_id = ANY($1::uuid[])
|
||||||
|
"#,
|
||||||
|
&ids,
|
||||||
|
)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let clients: Result<HashMap<Ulid, Client<PostgresqlBackend>>, _> = res
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| r.try_into().map(|c: Client<PostgresqlBackend>| (c.data, c)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
clients
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(client.id = %id),
|
fields(client.id = %id),
|
||||||
|
@ -12,11 +12,21 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
use mas_data_model::Session;
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use sqlx::PgExecutor;
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use mas_data_model::{BrowserSession, Session, User};
|
||||||
|
use sqlx::{postgres::PgArguments, Arguments, PgConnection, PgExecutor};
|
||||||
|
use tracing::{info_span, Instrument};
|
||||||
|
use ulid::Ulid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{Clock, PostgresqlBackend};
|
use self::client::lookup_clients;
|
||||||
|
use crate::{
|
||||||
|
pagination::{generate_pagination, process_page},
|
||||||
|
user::lookup_active_session,
|
||||||
|
Clock, PostgresqlBackend,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod access_token;
|
pub mod access_token;
|
||||||
pub mod authorization_grant;
|
pub mod authorization_grant;
|
||||||
@ -56,3 +66,113 @@ pub async fn end_oauth_session(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct OAuthSessionLookup {
|
||||||
|
oauth2_session_id: Uuid,
|
||||||
|
user_session_id: Uuid,
|
||||||
|
oauth2_client_id: Uuid,
|
||||||
|
scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
user.id = %user.data,
|
||||||
|
user.username = user.username,
|
||||||
|
),
|
||||||
|
err(Display),
|
||||||
|
)]
|
||||||
|
pub async fn get_paginated_user_oauth_sessions(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
user: &User<PostgresqlBackend>,
|
||||||
|
before: Option<Ulid>,
|
||||||
|
after: Option<Ulid>,
|
||||||
|
first: Option<usize>,
|
||||||
|
last: Option<usize>,
|
||||||
|
) -> Result<(bool, bool, Vec<Session<PostgresqlBackend>>), anyhow::Error> {
|
||||||
|
let mut query = String::from(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
os.oauth2_session_id,
|
||||||
|
os.user_session_id,
|
||||||
|
os.oauth2_client_id,
|
||||||
|
os.scope,
|
||||||
|
os.created_at,
|
||||||
|
os.finished_at
|
||||||
|
FROM oauth2_sessions os
|
||||||
|
LEFT JOIN user_sessions us
|
||||||
|
USING (user_session_id)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut arguments = PgArguments::default();
|
||||||
|
|
||||||
|
query += " WHERE us.user_id = ";
|
||||||
|
arguments.add(Uuid::from(user.data));
|
||||||
|
arguments.format_placeholder(&mut query)?;
|
||||||
|
|
||||||
|
generate_pagination(
|
||||||
|
&mut query,
|
||||||
|
"oauth2_session_id",
|
||||||
|
&mut arguments,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let page: Vec<OAuthSessionLookup> = sqlx::query_as_with(&query, arguments)
|
||||||
|
.fetch_all(&mut *conn)
|
||||||
|
.instrument(info_span!(
|
||||||
|
"Fetch paginated user oauth sessions",
|
||||||
|
query = query
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, page) = process_page(page, first, last)?;
|
||||||
|
|
||||||
|
let client_ids: BTreeSet<Ulid> = page
|
||||||
|
.iter()
|
||||||
|
.map(|i| Ulid::from(i.oauth2_client_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let browser_session_ids: BTreeSet<Ulid> =
|
||||||
|
page.iter().map(|i| Ulid::from(i.user_session_id)).collect();
|
||||||
|
|
||||||
|
let clients = lookup_clients(&mut *conn, client_ids).await?;
|
||||||
|
|
||||||
|
// TODO: this can generate N queries instead of batching. This is less than
|
||||||
|
// ideal
|
||||||
|
let mut browser_sessions: HashMap<Ulid, BrowserSession<PostgresqlBackend>> = HashMap::new();
|
||||||
|
for id in browser_session_ids {
|
||||||
|
let v = lookup_active_session(&mut *conn, id).await?;
|
||||||
|
browser_sessions.insert(id, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
let page: Result<Vec<_>, _> = page
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| {
|
||||||
|
let client = clients
|
||||||
|
.get(&Ulid::from(item.oauth2_client_id))
|
||||||
|
.context("client was not fetched")?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let browser_session = browser_sessions
|
||||||
|
.get(&Ulid::from(item.user_session_id))
|
||||||
|
.context("browser session was not fetched")?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let scope = item.scope.parse()?;
|
||||||
|
|
||||||
|
anyhow::Ok(Session {
|
||||||
|
data: Ulid::from(item.oauth2_session_id),
|
||||||
|
client,
|
||||||
|
browser_session,
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((has_previous_page, has_next_page, page?))
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user