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

frontend: Show all compatibilities sessions, not just SSO logins

Also cleans up a bunch of things in the frontend
This commit is contained in:
Quentin Gliech
2023-07-06 17:49:50 +02:00
parent 76653f9638
commit ca520dfd9a
25 changed files with 708 additions and 369 deletions

View File

@ -24,7 +24,10 @@ use crate::state::ContextExt;
/// A compat session represents a client session which used the legacy Matrix
/// login API.
#[derive(Description)]
pub struct CompatSession(pub mas_data_model::CompatSession);
pub struct CompatSession(
pub mas_data_model::CompatSession,
pub Option<mas_data_model::CompatSsoLogin>,
);
#[Object(use_type_description)]
impl CompatSession {
@ -61,6 +64,11 @@ impl CompatSession {
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
self.0.finished_at()
}
/// The associated SSO login, if any.
pub async fn sso_login(&self) -> Option<CompatSsoLogin> {
self.1.as_ref().map(|l| CompatSsoLogin(l.clone()))
}
}
/// A compat SSO login represents a login done through the legacy Matrix login
@ -114,6 +122,6 @@ impl CompatSsoLogin {
.context("Could not load compat session")?;
repo.cancel().await?;
Ok(Some(CompatSession(session)))
Ok(Some(CompatSession(session, Some(self.0.clone()))))
}
}

View File

@ -22,14 +22,17 @@ use mas_storage::{
oauth2::OAuth2SessionRepository,
upstream_oauth2::UpstreamOAuthLinkRepository,
user::{BrowserSessionRepository, UserEmailRepository},
Pagination,
Pagination, RepositoryAccess,
};
use super::{
compat_sessions::CompatSsoLogin, BrowserSession, Cursor, NodeCursor, NodeType, OAuth2Session,
UpstreamOAuth2Link,
};
use crate::{model::matrix::MatrixUser, state::ContextExt};
use crate::{
model::{matrix::MatrixUser, CompatSession},
state::ContextExt,
};
#[derive(Description)]
/// A user is an individual's account.
@ -129,6 +132,58 @@ impl User {
.await
}
/// Get the list of compatibility sessions, chronologically sorted
async fn compat_sessions(
&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, CompatSession>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let after_id = after
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let page = repo
.compat_session()
.list_paginated(&self.0, pagination)
.await?;
repo.cancel().await?;
let mut connection = Connection::new(page.has_previous_page, page.has_next_page);
connection
.edges
.extend(page.edges.into_iter().map(|(session, sso_login)| {
Edge::new(
OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
CompatSession(session, sso_login),
)
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
/// Get the list of active browser sessions, chronologically sorted
async fn browser_sessions(
&self,

View File

@ -66,7 +66,8 @@ impl EndCompatSessionPayload {
/// Returns the ended session.
async fn compat_session(&self) -> Option<CompatSession> {
match self {
Self::Ended(session) => Some(CompatSession(session.clone())),
// XXX: the SSO login is not returned here.
Self::Ended(session) => Some(CompatSession(session.clone(), None)),
Self::NotFound => None,
}
}

View File

@ -40,7 +40,9 @@ use axum::{
Router,
};
use headers::HeaderName;
use hyper::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_TYPE};
use hyper::header::{
ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_TYPE,
};
use mas_http::CorsLayerExt;
use mas_keystore::{Encrypter, Keystore};
use mas_policy::PolicyFactory;
@ -268,7 +270,8 @@ where
BoxRng: FromRequestParts<S>,
{
Router::new()
// TODO: mount this route somewhere else?
// XXX: hard-coded redirect from /account to /account/
.route("/account", get(|| async { mas_router::Account.go() }))
.route(mas_router::Account::route(), get(self::views::app::get))
.route(
mas_router::AccountWildcard::route(),
@ -351,6 +354,7 @@ where
if let Ok(res) = templates.render_error(ctx).await {
let (mut parts, _original_body) = response.into_parts();
parts.headers.remove(CONTENT_TYPE);
parts.headers.remove(CONTENT_LENGTH);
return Ok((parts, Html(res)).into_response());
}
}

View File

@ -14,14 +14,20 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use mas_data_model::{CompatSession, CompatSessionState, Device, User};
use mas_storage::{compat::CompatSessionRepository, Clock};
use mas_data_model::{
CompatSession, CompatSessionState, CompatSsoLogin, CompatSsoLoginState, Device, User,
};
use mas_storage::{compat::CompatSessionRepository, Clock, Page, Pagination};
use rand::RngCore;
use sqlx::PgConnection;
use sqlx::{PgConnection, QueryBuilder};
use ulid::Ulid;
use url::Url;
use uuid::Uuid;
use crate::{tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError, LookupResultExt};
use crate::{
pagination::QueryBuilderExt, tracing::ExecuteExt, DatabaseError, DatabaseInconsistencyError,
LookupResultExt,
};
/// An implementation of [`CompatSessionRepository`] for a PostgreSQL connection
pub struct PgCompatSessionRepository<'c> {
@ -75,6 +81,101 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
}
}
#[derive(sqlx::FromRow)]
struct CompatSessionAndSsoLoginLookup {
compat_session_id: Uuid,
device_id: String,
user_id: Uuid,
created_at: DateTime<Utc>,
finished_at: Option<DateTime<Utc>>,
is_synapse_admin: bool,
compat_sso_login_id: Option<Uuid>,
compat_sso_login_token: Option<String>,
compat_sso_login_redirect_uri: Option<String>,
compat_sso_login_created_at: Option<DateTime<Utc>>,
compat_sso_login_fulfilled_at: Option<DateTime<Utc>>,
compat_sso_login_exchanged_at: Option<DateTime<Utc>>,
}
impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSsoLogin>) {
type Error = DatabaseInconsistencyError;
fn try_from(value: CompatSessionAndSsoLoginLookup) -> Result<Self, Self::Error> {
let id = value.compat_session_id.into();
let device = Device::try_from(value.device_id).map_err(|e| {
DatabaseInconsistencyError::on("compat_sessions")
.column("device_id")
.row(id)
.source(e)
})?;
let state = match value.finished_at {
None => CompatSessionState::Valid,
Some(finished_at) => CompatSessionState::Finished { finished_at },
};
let session = CompatSession {
id,
state,
user_id: value.user_id.into(),
device,
created_at: value.created_at,
is_synapse_admin: value.is_synapse_admin,
};
match (
value.compat_sso_login_id,
value.compat_sso_login_token,
value.compat_sso_login_redirect_uri,
value.compat_sso_login_created_at,
value.compat_sso_login_fulfilled_at,
value.compat_sso_login_exchanged_at,
) {
(None, None, None, None, None, None) => Ok((session, None)),
(
Some(id),
Some(login_token),
Some(redirect_uri),
Some(created_at),
fulfilled_at,
exchanged_at,
) => {
let id = id.into();
let redirect_uri = Url::parse(&redirect_uri).map_err(|e| {
DatabaseInconsistencyError::on("compat_sso_logins")
.column("redirect_uri")
.row(id)
.source(e)
})?;
let state = match (fulfilled_at, exchanged_at) {
(Some(fulfilled_at), None) => CompatSsoLoginState::Fulfilled {
fulfilled_at,
session_id: session.id,
},
(Some(fulfilled_at), Some(exchanged_at)) => CompatSsoLoginState::Exchanged {
fulfilled_at,
exchanged_at,
session_id: session.id,
},
_ => return Err(DatabaseInconsistencyError::on("compat_sso_logins").row(id)),
};
let login = CompatSsoLogin {
id,
redirect_uri,
login_token,
created_at,
state,
};
Ok((session, Some(login)))
}
_ => Err(DatabaseInconsistencyError::on("compat_sso_logins").row(id)),
}
}
}
#[async_trait]
impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
type Error = DatabaseError;
@ -201,4 +302,53 @@ impl<'c> CompatSessionRepository for PgCompatSessionRepository<'c> {
Ok(compat_session)
}
#[tracing::instrument(
name = "db.compat_session.list_paginated",
skip_all,
fields(
db.statement,
%user.id,
),
err,
)]
async fn list_paginated(
&mut self,
user: &User,
pagination: Pagination,
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error> {
let mut query = QueryBuilder::new(
r#"
SELECT cs.compat_session_id
, cs.device_id
, cs.user_id
, cs.created_at
, cs.finished_at
, cs.is_synapse_admin
, 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
FROM compat_sessions cs
LEFT JOIN compat_sso_logins cl USING (compat_session_id)
"#,
);
query
.push(" WHERE cs.user_id = ")
.push_bind(Uuid::from(user.id))
.generate_pagination("cs.compat_session_id", pagination);
let edges: Vec<CompatSessionAndSsoLoginLookup> = query
.build_query_as()
.traced()
.fetch_all(&mut *self.conn)
.await?;
let page = pagination.process(edges).try_map(TryFrom::try_from)?;
Ok(page)
}
}

View File

@ -13,11 +13,11 @@
// limitations under the License.
use async_trait::async_trait;
use mas_data_model::{CompatSession, Device, User};
use mas_data_model::{CompatSession, CompatSsoLogin, Device, User};
use rand_core::RngCore;
use ulid::Ulid;
use crate::{repository_impl, Clock};
use crate::{repository_impl, Clock, Page, Pagination};
/// A [`CompatSessionRepository`] helps interacting with
/// [`CompatSessionRepository`] saved in the storage backend
@ -80,6 +80,24 @@ pub trait CompatSessionRepository: Send + Sync {
clock: &dyn Clock,
compat_session: CompatSession,
) -> Result<CompatSession, Self::Error>;
/// Get a paginated list of compat sessions for a user
///
/// Returns a page of compat sessions, with the associated SSO logins if any
///
/// # Parameters
///
/// * `user`: The user to get the compat sessions for
/// * `pagination`: The pagination parameters
///
/// # Errors
///
/// Returns [`Self::Error`] if the underlying repository fails
async fn list_paginated(
&mut self,
user: &User,
pagination: Pagination,
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
}
repository_impl!(CompatSessionRepository:
@ -99,4 +117,10 @@ repository_impl!(CompatSessionRepository:
clock: &dyn Clock,
compat_session: CompatSession,
) -> Result<CompatSession, Self::Error>;
async fn list_paginated(
&mut self,
user: &User,
pagination: Pagination,
) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
);