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
frontend: Show all compatibilities sessions, not just SSO logins
Also cleans up a bunch of things in the frontend
This commit is contained in:
@ -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()))))
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
);
|
||||
|
Reference in New Issue
Block a user