Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Commit

Permalink
Save which user session created a compat session
Browse files Browse the repository at this point in the history
This also exposes the user session in the GraphQL API, and allow
filtering on browser session ID on the app session list.
  • Loading branch information
sandhose committed Feb 21, 2024
1 parent 03b6ad7 commit d3ec801
Show file tree
Hide file tree
Showing 21 changed files with 433 additions and 52 deletions.
2 changes: 1 addition & 1 deletion crates/cli/src/commands/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ impl Options {

let compat_session = repo
.compat_session()
.add(&mut rng, &clock, &user, device, admin)
.add(&mut rng, &clock, &user, device, None, admin)

Check warning on line 187 in crates/cli/src/commands/manage.rs

View check run for this annotation

Codecov / codecov/patch

crates/cli/src/commands/manage.rs#L187

Added line #L187 was not covered by tests
.await?;

let token = TokenType::CompatAccessToken.generate(&mut rng);
Expand Down
1 change: 1 addition & 0 deletions crates/data-model/src/compat/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub struct CompatSession {
pub state: CompatSessionState,
pub user_id: Ulid,
pub device: Device,
pub user_session_id: Option<Ulid>,
pub created_at: DateTime<Utc>,
pub is_synapse_admin: bool,
pub last_active_at: Option<DateTime<Utc>>,
Expand Down
108 changes: 104 additions & 4 deletions crates/graphql/src/model/browser_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use async_graphql::{Context, Description, Object, ID};
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Description, Object, ID,
};
use chrono::{DateTime, Utc};
use mas_storage::{user::BrowserSessionRepository, RepositoryAccess};

use super::{NodeType, SessionState, User};
use mas_data_model::Device;
use mas_storage::{
app_session::AppSessionFilter, user::BrowserSessionRepository, Pagination, RepositoryAccess,
};

use super::{
AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
SessionState, User,
};
use crate::state::ContextExt;

/// A browser session represents a logged in user in a browser.
Expand Down Expand Up @@ -92,6 +101,97 @@ impl BrowserSession {
pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
self.0.last_active_at
}

/// Get the list of both compat and OAuth 2.0 sessions started by this
/// browser session, 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(name = "device", desc = "List only sessions for the given device.")]
device_param: Option<String>,

#[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?;

Check warning on line 126 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L108-L126

Added lines #L108 - L126 were not covered by tests

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)?;

Check warning on line 144 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L128-L144

Added lines #L128 - L144 were not covered by tests

let device_param = device_param.map(Device::try_from).transpose()?;

Check warning on line 146 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L146

Added line #L146 was not covered by tests

let filter = AppSessionFilter::new().for_browser_session(&self.0);

Check warning on line 148 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L148

Added line #L148 was not covered by tests

let filter = match state_param {
Some(SessionState::Active) => filter.active_only(),
Some(SessionState::Finished) => filter.finished_only(),
None => filter,

Check warning on line 153 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L150-L153

Added lines #L150 - L153 were not covered by tests
};

let filter = match device_param.as_ref() {
Some(device) => filter.for_device(device),
None => filter,

Check warning on line 158 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L156-L158

Added lines #L156 - L158 were not covered by tests
};

let page = repo.app_session().list(filter, pagination).await?;

Check warning on line 161 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L161

Added line #L161 was not covered by tests

let count = if ctx.look_ahead().field("totalCount").exists() {
Some(repo.app_session().count(filter).await?)

Check warning on line 164 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L163-L164

Added lines #L163 - L164 were not covered by tests
} else {
None

Check warning on line 166 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L166

Added line #L166 was not covered by tests
};

repo.cancel().await?;

Check warning on line 169 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L169

Added line #L169 was not covered by tests

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
}

Check warning on line 194 in crates/graphql/src/model/browser_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/browser_sessions.rs#L171-L194

Added lines #L171 - L194 were not covered by tests
}

/// An authentication records when a user enter their credential in a browser
Expand Down
23 changes: 22 additions & 1 deletion crates/graphql/src/model/compat_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use chrono::{DateTime, Utc};
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
use url::Url;

use super::{NodeType, SessionState, User};
use super::{BrowserSession, NodeType, SessionState, User};
use crate::state::ContextExt;

/// Lazy-loaded reverse reference.
Expand Down Expand Up @@ -125,6 +125,27 @@ impl CompatSession {
Ok(sso_login.map(CompatSsoLogin))
}

/// The browser session which started this session, if any.
pub async fn browser_session(
&self,
ctx: &Context<'_>,
) -> Result<Option<BrowserSession>, async_graphql::Error> {
let Some(user_session_id) = self.session.user_session_id else {
return Ok(None);

Check warning on line 134 in crates/graphql/src/model/compat_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/compat_sessions.rs#L129-L134

Added lines #L129 - L134 were not covered by tests
};

let state = ctx.state();
let mut repo = state.repository().await?;
let browser_session = repo
.browser_session()
.lookup(user_session_id)
.await?
.context("Could not load browser session")?;
repo.cancel().await?;

Check warning on line 144 in crates/graphql/src/model/compat_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/compat_sessions.rs#L137-L144

Added lines #L137 - L144 were not covered by tests

Ok(Some(BrowserSession(browser_session)))
}

Check warning on line 147 in crates/graphql/src/model/compat_sessions.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/compat_sessions.rs#L146-L147

Added lines #L146 - L147 were not covered by tests

/// The state of the session.
pub async fn state(&self) -> SessionState {
match &self.session.state {
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub use self::{
node::{Node, NodeType},
oauth::{OAuth2Client, OAuth2Session},
upstream_oauth::{UpstreamOAuth2Link, UpstreamOAuth2Provider},
users::{User, UserEmail},
users::{AppSession, User, UserEmail},
viewer::{Anonymous, Viewer, ViewerSession},
};

Expand Down
42 changes: 41 additions & 1 deletion crates/graphql/src/model/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use anyhow::Context as _;
use async_graphql::{
connection::{query, Connection, Edge, OpaqueCursor},
Context, Description, Enum, Object, Union, ID,
Expand Down Expand Up @@ -544,6 +545,12 @@ impl User {
#[graphql(name = "device", desc = "List only sessions for the given device.")]
device_param: Option<String>,

#[graphql(
name = "browserSession",
desc = "List only sessions for the given session."
)]
browser_session_param: Option<ID>,

Check warning on line 553 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L548-L553

Added lines #L548 - L553 were not covered by tests
#[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.")]
Expand All @@ -552,6 +559,7 @@ impl User {
#[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 requester = ctx.requester();

Check warning on line 562 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L562

Added line #L562 was not covered by tests
let mut repo = state.repository().await?;

query(
Expand Down Expand Up @@ -587,6 +595,38 @@ impl User {
None => filter,
};

let maybe_session = match browser_session_param {
Some(id) => {

Check warning on line 599 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L598-L599

Added lines #L598 - L599 were not covered by tests
// This might fail, but we're probably alright with it
let id = NodeType::BrowserSession
.extract_ulid(&id)
.context("Invalid browser_session parameter")?;

Check warning on line 603 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L601-L603

Added lines #L601 - L603 were not covered by tests

let Some(session) = repo
.browser_session()
.lookup(id)
.await?
.filter(|u| requester.is_owner_or_admin(u))

Check warning on line 609 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L605-L609

Added lines #L605 - L609 were not covered by tests
else {
// If we couldn't find the session or if the requester can't access it,
// return an empty list
return Ok(Connection::with_additional_fields(
false,
false,
PreloadedTotalCount(Some(0)),
));

Check warning on line 617 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L613-L617

Added lines #L613 - L617 were not covered by tests
};

Some(session)

Check warning on line 620 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L620

Added line #L620 was not covered by tests
}
None => None,

Check warning on line 622 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L622

Added line #L622 was not covered by tests
};

let filter = match maybe_session {
Some(ref session) => filter.for_browser_session(session),
None => filter,

Check warning on line 627 in crates/graphql/src/model/users.rs

View check run for this annotation

Codecov / codecov/patch

crates/graphql/src/model/users.rs#L625-L627

Added lines #L625 - L627 were not covered by tests
};

let page = repo.app_session().list(filter, pagination).await?;

let count = if ctx.look_ahead().field("totalCount").exists() {
Expand Down Expand Up @@ -625,7 +665,7 @@ impl User {

/// A session in an application, either a compatibility or an OAuth 2.0 one
#[derive(Union)]
enum AppSession {
pub enum AppSession {
CompatSession(Box<CompatSession>),
OAuth2Session(Box<OAuth2Session>),
}
Expand Down
11 changes: 9 additions & 2 deletions crates/handlers/src/compat/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ async fn user_password_login(

let session = repo
.compat_session()
.add(&mut rng, clock, &user, device, false)
.add(&mut rng, clock, &user, device, None, false)
.await?;

Ok((session, user))
Expand Down Expand Up @@ -738,7 +738,14 @@ mod tests {
// Complete the flow by fulfilling it with a session
let compat_session = repo
.compat_session()
.add(&mut state.rng(), &state.clock, user, device.clone(), false)
.add(
&mut state.rng(),
&state.clock,
user,
device.clone(),
None,
false,
)
.await
.unwrap();

Expand Down
9 changes: 8 additions & 1 deletion crates/handlers/src/compat/login_sso_complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,14 @@ pub async fn post(

let compat_session = repo
.compat_session()
.add(&mut rng, &clock, &session.user, device, false)
.add(
&mut rng,
&clock,
&session.user,
device,
Some(&session),
false,
)
.await?;

repo.compat_sso_login()
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d3ec801

Please sign in to comment.