You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
GraphQL API: compat sessions
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -188,6 +188,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2605,6 +2606,7 @@ dependencies = [
|
|||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ulid",
|
"ulid",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@@ -6,12 +6,13 @@ edition = "2021"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-graphql = { version = "4.0.16", features = ["chrono"] }
|
async-graphql = { version = "4.0.16", features = ["chrono", "url"] }
|
||||||
chrono = "0.4.22"
|
chrono = "0.4.22"
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres"] }
|
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres"] }
|
||||||
tokio = { version = "1.21.2", features = ["time"] }
|
tokio = { version = "1.21.2", features = ["time"] }
|
||||||
ulid = "1.0.0"
|
ulid = "1.0.0"
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
mas-axum-utils = { path = "../axum-utils" }
|
mas-axum-utils = { path = "../axum-utils" }
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
mod browser_sessions;
|
mod browser_sessions;
|
||||||
|
mod compat_sessions;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
|
88
crates/graphql/src/model/compat_sessions.rs
Normal file
88
crates/graphql/src/model/compat_sessions.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// 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::{Object, ID};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use mas_data_model::CompatSsoLoginState;
|
||||||
|
use mas_storage::PostgresqlBackend;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
|
||||||
|
pub struct CompatSession(pub mas_data_model::CompatSession<PostgresqlBackend>);
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl CompatSession {
|
||||||
|
async fn id(&self) -> ID {
|
||||||
|
ID(self.0.data.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn user(&self) -> User {
|
||||||
|
User(self.0.user.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn device_id(&self) -> &str {
|
||||||
|
self.0.device.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.0.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finished_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.0.finished_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CompatSsoLogin(pub mas_data_model::CompatSsoLogin<PostgresqlBackend>);
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl CompatSsoLogin {
|
||||||
|
async fn id(&self) -> ID {
|
||||||
|
ID(self.0.data.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.0.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn redirect_uri(&self) -> &Url {
|
||||||
|
&self.0.redirect_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
match &self.0.state {
|
||||||
|
CompatSsoLoginState::Pending => None,
|
||||||
|
CompatSsoLoginState::Fulfilled { fulfilled_at, .. }
|
||||||
|
| CompatSsoLoginState::Exchanged { fulfilled_at, .. } => Some(*fulfilled_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchanged_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
match &self.0.state {
|
||||||
|
CompatSsoLoginState::Pending | CompatSsoLoginState::Fulfilled { .. } => None,
|
||||||
|
CompatSsoLoginState::Exchanged { exchanged_at, .. } => Some(*exchanged_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn session(&self) -> Option<CompatSession> {
|
||||||
|
match &self.0.state {
|
||||||
|
CompatSsoLoginState::Pending => None,
|
||||||
|
CompatSsoLoginState::Fulfilled { session, .. }
|
||||||
|
| CompatSsoLoginState::Exchanged { session, .. } => {
|
||||||
|
Some(CompatSession(session.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -21,6 +21,7 @@ use ulid::Ulid;
|
|||||||
pub enum NodeType {
|
pub enum NodeType {
|
||||||
UserEmail,
|
UserEmail,
|
||||||
BrowserSession,
|
BrowserSession,
|
||||||
|
CompatSsoLogin,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@@ -20,7 +20,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use mas_storage::PostgresqlBackend;
|
use mas_storage::PostgresqlBackend;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use super::{BrowserSession, Cursor, NodeCursor, NodeType};
|
use super::{compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType};
|
||||||
|
|
||||||
pub struct User(pub mas_data_model::User<PostgresqlBackend>);
|
pub struct User(pub mas_data_model::User<PostgresqlBackend>);
|
||||||
|
|
||||||
@@ -50,6 +50,50 @@ impl User {
|
|||||||
self.0.primary_email.clone().map(UserEmail)
|
self.0.primary_email.clone().map(UserEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn compat_sso_logins(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i32>,
|
||||||
|
last: Option<i32>,
|
||||||
|
) -> Result<Connection<Cursor, CompatSsoLogin>, 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::UserEmail))
|
||||||
|
.transpose()?;
|
||||||
|
let before_id = before
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, edges) =
|
||||||
|
mas_storage::compat::get_paginated_user_compat_sso_logins(
|
||||||
|
&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(|u| {
|
||||||
|
Edge::new(
|
||||||
|
OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, u.data)),
|
||||||
|
CompatSsoLogin(u),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok::<_, async_graphql::Error>(connection)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn browser_sessions(
|
async fn browser_sessions(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
@@ -20,7 +20,7 @@ use mas_data_model::{
|
|||||||
Device, User, UserEmail,
|
Device, User, UserEmail,
|
||||||
};
|
};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sqlx::{Acquire, PgExecutor, Postgres};
|
use sqlx::{postgres::PgArguments, Acquire, Arguments, PgExecutor, Postgres};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{info_span, Instrument};
|
use tracing::{info_span, Instrument};
|
||||||
@@ -28,7 +28,11 @@ use ulid::Ulid;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{user::lookup_user_by_username, Clock, DatabaseInconsistencyError, PostgresqlBackend};
|
use crate::{
|
||||||
|
pagination::{generate_pagination, process_page},
|
||||||
|
user::lookup_user_by_username,
|
||||||
|
Clock, DatabaseInconsistencyError, PostgresqlBackend,
|
||||||
|
};
|
||||||
|
|
||||||
struct CompatAccessTokenLookup {
|
struct CompatAccessTokenLookup {
|
||||||
compat_access_token_id: Uuid,
|
compat_access_token_id: Uuid,
|
||||||
@@ -632,6 +636,7 @@ pub async fn insert_compat_sso_login(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
struct CompatSsoLoginLookup {
|
struct CompatSsoLoginLookup {
|
||||||
compat_sso_login_id: Uuid,
|
compat_sso_login_id: Uuid,
|
||||||
compat_sso_login_token: String,
|
compat_sso_login_token: String,
|
||||||
@@ -803,6 +808,83 @@ pub async fn get_compat_sso_login_by_id(
|
|||||||
Ok(res.try_into()?)
|
Ok(res.try_into()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
user.id = %user.data,
|
||||||
|
user.username = user.username,
|
||||||
|
),
|
||||||
|
err(Display),
|
||||||
|
)]
|
||||||
|
pub async fn get_paginated_user_compat_sso_logins(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
user: &User<PostgresqlBackend>,
|
||||||
|
before: Option<Ulid>,
|
||||||
|
after: Option<Ulid>,
|
||||||
|
first: Option<usize>,
|
||||||
|
last: Option<usize>,
|
||||||
|
) -> Result<(bool, bool, Vec<CompatSsoLogin<PostgresqlBackend>>), anyhow::Error> {
|
||||||
|
// TODO: this queries too much (like user info) which we probably don't need
|
||||||
|
// because we already have them
|
||||||
|
let mut query = String::from(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
cl.compat_sso_login_id,
|
||||||
|
cl.login_token AS "compat_sso_login_token",
|
||||||
|
cl.redirect_uri AS "compat_sso_login_redirect_uri",
|
||||||
|
cl.created_at AS "compat_sso_login_created_at",
|
||||||
|
cl.fulfilled_at AS "compat_sso_login_fulfilled_at",
|
||||||
|
cl.exchanged_at AS "compat_sso_login_exchanged_at",
|
||||||
|
cs.compat_session_id AS "compat_session_id",
|
||||||
|
cs.created_at AS "compat_session_created_at",
|
||||||
|
cs.finished_at AS "compat_session_finished_at",
|
||||||
|
cs.device_id AS "compat_session_device_id",
|
||||||
|
u.user_id AS "user_id",
|
||||||
|
u.username AS "user_username",
|
||||||
|
ue.user_email_id AS "user_email_id",
|
||||||
|
ue.email AS "user_email",
|
||||||
|
ue.created_at AS "user_email_created_at",
|
||||||
|
ue.confirmed_at AS "user_email_confirmed_at"
|
||||||
|
FROM compat_sso_logins cl
|
||||||
|
LEFT JOIN compat_sessions cs
|
||||||
|
USING (compat_session_id)
|
||||||
|
LEFT JOIN users u
|
||||||
|
USING (user_id)
|
||||||
|
LEFT JOIN user_emails ue
|
||||||
|
ON ue.user_email_id = u.primary_user_email_id
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut arguments = PgArguments::default();
|
||||||
|
|
||||||
|
query += " WHERE cs.user_id = ";
|
||||||
|
arguments.add(Uuid::from(user.data));
|
||||||
|
arguments.format_placeholder(&mut query)?;
|
||||||
|
|
||||||
|
generate_pagination(
|
||||||
|
&mut query,
|
||||||
|
"cl.compat_sso_login_id",
|
||||||
|
&mut arguments,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let page: Vec<CompatSsoLoginLookup> = sqlx::query_as_with(&query, arguments)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.instrument(info_span!(
|
||||||
|
"Fetch paginated user compat SSO logins",
|
||||||
|
query = query
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, page) = process_page(page, first, last)?;
|
||||||
|
|
||||||
|
let page: Result<Vec<_>, _> = page.into_iter().map(TryInto::try_into).collect();
|
||||||
|
Ok((has_previous_page, has_next_page, page?))
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub async fn get_compat_sso_login_by_token(
|
pub async fn get_compat_sso_login_by_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
|
Reference in New Issue
Block a user