1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Expose a unified session list in the GraphQL API

This commit is contained in:
Quentin Gliech
2023-09-20 20:18:02 +02:00
parent f1d420f381
commit d91b0e20e4
13 changed files with 459 additions and 177 deletions

View File

@ -12,23 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{Context, Description, Enum, Object, ID};
use async_graphql::{Context, Description, Object, ID};
use chrono::{DateTime, Utc};
use mas_storage::{user::BrowserSessionRepository, RepositoryAccess};
use super::{NodeType, User};
use super::{NodeType, SessionState, User};
use crate::state::ContextExt;
/// The state of a browser session.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum BrowserSessionState {
/// The session is active.
Active,
/// The session is no longer active.
Finished,
}
/// A browser session represents a logged in user in a browser.
#[derive(Description)]
pub struct BrowserSession(pub mas_data_model::BrowserSession);
@ -80,11 +70,11 @@ impl BrowserSession {
}
/// The state of the session.
pub async fn state(&self) -> BrowserSessionState {
pub async fn state(&self) -> SessionState {
if self.0.finished_at.is_some() {
BrowserSessionState::Finished
SessionState::Finished
} else {
BrowserSessionState::Active
SessionState::Active
}
}

View File

@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
use url::Url;
use super::{NodeType, User};
use super::{NodeType, SessionState, User};
use crate::state::ContextExt;
/// Lazy-loaded reverse reference.
@ -57,16 +57,6 @@ impl CompatSession {
}
}
/// The state of a compatibility session.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum CompatSessionState {
/// The session is active.
Active,
/// The session is no longer active.
Finished,
}
/// The type of a compatibility session.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum CompatSessionType {
@ -136,10 +126,10 @@ impl CompatSession {
}
/// The state of the session.
pub async fn state(&self) -> CompatSessionState {
pub async fn state(&self) -> SessionState {
match &self.session.state {
mas_data_model::CompatSessionState::Valid => CompatSessionState::Active,
mas_data_model::CompatSessionState::Finished { .. } => CompatSessionState::Finished,
mas_data_model::CompatSessionState::Valid => SessionState::Active,
mas_data_model::CompatSessionState::Finished { .. } => SessionState::Finished,
}
}

View File

@ -22,6 +22,14 @@ pub use super::NodeType;
pub struct NodeCursor(pub NodeType, pub Ulid);
impl NodeCursor {
pub fn extract_for_types(&self, node_types: &[NodeType]) -> Result<Ulid, async_graphql::Error> {
if node_types.contains(&self.0) {
Ok(self.1)
} else {
Err(async_graphql::Error::new("invalid cursor"))
}
}
pub fn extract_for_type(&self, node_type: NodeType) -> Result<Ulid, async_graphql::Error> {
if self.0 == node_type {
Ok(self.1)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use async_graphql::{Interface, Object};
use async_graphql::{Enum, Interface, Object};
use chrono::{DateTime, Utc};
mod browser_sessions;
@ -63,3 +63,13 @@ impl PreloadedTotalCount {
.ok_or_else(|| async_graphql::Error::new("total count not preloaded"))
}
}
/// The state of a session
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum SessionState {
/// The session is active.
Active,
/// The session is no longer active.
Finished,
}

View File

@ -15,25 +15,14 @@
use anyhow::Context as _;
use async_graphql::{Context, Description, Enum, Object, ID};
use chrono::{DateTime, Utc};
use mas_data_model::SessionState;
use mas_storage::{oauth2::OAuth2ClientRepository, user::BrowserSessionRepository};
use oauth2_types::{oidc::ApplicationType, scope::Scope};
use ulid::Ulid;
use url::Url;
use super::{BrowserSession, NodeType, User};
use super::{BrowserSession, NodeType, SessionState, User};
use crate::{state::ContextExt, UserId};
/// The state of an OAuth 2.0 session.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum OAuth2SessionState {
/// The session is active.
Active,
/// The session is no longer active.
Finished,
}
/// An OAuth 2.0 session represents a client session which used the OAuth APIs
/// to login.
#[derive(Description)]
@ -73,16 +62,16 @@ impl OAuth2Session {
/// When the session ended.
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
match &self.0.state {
SessionState::Valid => None,
SessionState::Finished { finished_at } => Some(*finished_at),
mas_data_model::SessionState::Valid => None,
mas_data_model::SessionState::Finished { finished_at } => Some(*finished_at),
}
}
/// The state of the session.
pub async fn state(&self) -> OAuth2SessionState {
pub async fn state(&self) -> SessionState {
match &self.0.state {
SessionState::Valid => OAuth2SessionState::Active,
SessionState::Finished { .. } => OAuth2SessionState::Finished,
mas_data_model::SessionState::Valid => SessionState::Active,
mas_data_model::SessionState::Finished { .. } => SessionState::Finished,
}
}

View File

@ -14,10 +14,11 @@
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Description, Enum, Object, ID,
Context, Description, Enum, Object, Union, ID,
};
use chrono::{DateTime, Utc};
use mas_storage::{
app_session::AppSessionFilter,
compat::{CompatSessionFilter, CompatSsoLoginFilter, CompatSsoLoginRepository},
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
upstream_oauth2::{UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository},
@ -26,12 +27,10 @@ use mas_storage::{
};
use super::{
browser_sessions::BrowserSessionState,
compat_sessions::{CompatSessionState, CompatSessionType, CompatSsoLogin},
compat_sessions::{CompatSessionType, CompatSsoLogin},
matrix::MatrixUser,
oauth::OAuth2SessionState,
BrowserSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session,
PreloadedTotalCount, UpstreamOAuth2Link,
PreloadedTotalCount, SessionState, UpstreamOAuth2Link,
};
use crate::state::ContextExt;
@ -160,7 +159,7 @@ impl User {
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions with the given state.")]
state_param: Option<CompatSessionState>,
state_param: Option<SessionState>,
#[graphql(name = "type", desc = "List only sessions with the given type.")]
type_param: Option<CompatSessionType>,
@ -192,8 +191,8 @@ impl User {
// Build the query filter
let filter = CompatSessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(CompatSessionState::Active) => filter.active_only(),
Some(CompatSessionState::Finished) => filter.finished_only(),
Some(SessionState::Active) => filter.active_only(),
Some(SessionState::Finished) => filter.finished_only(),
None => filter,
};
let filter = match type_param {
@ -239,7 +238,7 @@ impl User {
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions in the given state.")]
state_param: Option<BrowserSessionState>,
state_param: Option<SessionState>,
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
after: Option<String>,
@ -267,8 +266,8 @@ impl User {
let filter = BrowserSessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(BrowserSessionState::Active) => filter.active_only(),
Some(BrowserSessionState::Finished) => filter.finished_only(),
Some(SessionState::Active) => filter.active_only(),
Some(SessionState::Finished) => filter.finished_only(),
None => filter,
};
@ -377,7 +376,7 @@ impl User {
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions in the given state.")]
state_param: Option<OAuth2SessionState>,
state_param: Option<SessionState>,
#[graphql(desc = "List only sessions for the given client.")] client: Option<ID>,
@ -422,8 +421,8 @@ impl User {
let filter = OAuth2SessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(OAuth2SessionState::Active) => filter.active_only(),
Some(OAuth2SessionState::Finished) => filter.finished_only(),
Some(SessionState::Active) => filter.active_only(),
Some(SessionState::Finished) => filter.finished_only(),
None => filter,
};
@ -525,6 +524,94 @@ impl User {
)
.await
}
/// Get the list of both compat and OAuth 2.0 sessions, chronologically
/// sorted
#[allow(clippy::too_many_arguments)]
async fn app_sessions(
&self,
ctx: &Context<'_>,
#[graphql(name = "state", desc = "List only sessions in the given state.")]
state_param: Option<SessionState>,
#[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, AppSession, PreloadedTotalCount>, 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_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
})
.transpose()?;
let before_id = before
.map(|x: OpaqueCursor<NodeCursor>| {
x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
})
.transpose()?;
let pagination = Pagination::try_new(before_id, after_id, first, last)?;
let filter = AppSessionFilter::new().for_user(&self.0);
let filter = match state_param {
Some(SessionState::Active) => filter.active_only(),
Some(SessionState::Finished) => filter.finished_only(),
None => filter,
};
let page = repo.app_session().list(filter, pagination).await?;
let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.app_session().count(filter).await?)
} else {
None
};
repo.cancel().await?;
let mut connection = Connection::with_additional_fields(
page.has_previous_page,
page.has_next_page,
PreloadedTotalCount(count),
);
connection
.edges
.extend(page.edges.into_iter().map(|s| match s {
mas_storage::app_session::AppSession::Compat(session) => Edge::new(
OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
AppSession::CompatSession(Box::new(CompatSession::new(*session))),
),
mas_storage::app_session::AppSession::OAuth2(session) => Edge::new(
OpaqueCursor(NodeCursor(NodeType::OAuth2Session, session.id)),
AppSession::OAuth2Session(Box::new(OAuth2Session(*session))),
),
}));
Ok::<_, async_graphql::Error>(connection)
},
)
.await
}
}
/// A session in an application, either a compatibility or an OAuth 2.0 one
#[derive(Union)]
enum AppSession {
CompatSession(Box<CompatSession>),
OAuth2Session(Box<OAuth2Session>),
}
/// A user email address