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
Allow querying browser sessions
This commit is contained in:
@ -77,6 +77,7 @@ impl StorageBackendMarker for PostgresqlBackend {}
|
||||
|
||||
pub mod compat;
|
||||
pub mod oauth2;
|
||||
pub(crate) mod pagination;
|
||||
pub mod user;
|
||||
|
||||
/// Embedded migrations, allowing them to run on startup
|
||||
|
100
crates/storage/src/pagination.rs
Normal file
100
crates/storage/src/pagination.rs
Normal file
@ -0,0 +1,100 @@
|
||||
// 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 std::fmt::Write;
|
||||
|
||||
use sqlx::Arguments;
|
||||
use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn generate_pagination<'a, A, W>(
|
||||
query: &mut W,
|
||||
id_field: &'static str,
|
||||
arguments: &mut A,
|
||||
before: Option<Ulid>,
|
||||
after: Option<Ulid>,
|
||||
first: Option<usize>,
|
||||
last: Option<usize>,
|
||||
) -> Result<(), anyhow::Error>
|
||||
where
|
||||
W: Write,
|
||||
A: Arguments<'a>,
|
||||
Uuid: sqlx::Type<A::Database> + sqlx::Encode<'a, A::Database>,
|
||||
i64: sqlx::Type<A::Database> + sqlx::Encode<'a, A::Database>,
|
||||
{
|
||||
// ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564
|
||||
// 1. Start from the greedy query: SELECT * FROM table
|
||||
|
||||
// 2. If the after argument is provided, add `id > parsed_cursor` to the `WHERE`
|
||||
// clause
|
||||
if let Some(after) = after {
|
||||
write!(query, " AND {id_field} > ")?;
|
||||
arguments.add(Uuid::from(after));
|
||||
arguments.format_placeholder(query)?;
|
||||
}
|
||||
|
||||
// 3. If the before argument is provided, add `id < parsed_cursor` to the
|
||||
// `WHERE` clause
|
||||
if let Some(before) = before {
|
||||
write!(query, " AND {id_field} < ")?;
|
||||
arguments.add(Uuid::from(before));
|
||||
arguments.format_placeholder(query)?;
|
||||
}
|
||||
|
||||
// 4. If the first argument is provided, add `ORDER BY id ASC LIMIT first+1` to
|
||||
// the query
|
||||
if let Some(count) = first {
|
||||
write!(query, " ORDER BY {id_field} ASC LIMIT ")?;
|
||||
arguments.add((count + 1) as i64);
|
||||
arguments.format_placeholder(query)?;
|
||||
// 5. If the first argument is provided, add `ORDER BY id DESC LIMIT last+1`
|
||||
// to the query
|
||||
} else if let Some(count) = last {
|
||||
write!(query, " ORDER BY ue.user_email_id DESC LIMIT ")?;
|
||||
arguments.add((count + 1) as i64);
|
||||
arguments.format_placeholder(query)?;
|
||||
} else {
|
||||
anyhow::bail!("Either 'first' or 'last' must be specified");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_page<T>(
|
||||
mut page: Vec<T>,
|
||||
first: Option<usize>,
|
||||
last: Option<usize>,
|
||||
) -> Result<(bool, bool, Vec<T>), anyhow::Error> {
|
||||
let limit = match (first, last) {
|
||||
(Some(count), _) | (_, Some(count)) => count,
|
||||
_ => anyhow::bail!("Either 'first' or 'last' must be specified"),
|
||||
};
|
||||
|
||||
let is_full = page.len() == (limit + 1);
|
||||
if is_full {
|
||||
page.pop();
|
||||
}
|
||||
|
||||
let (has_previous_page, has_next_page) = if first.is_some() {
|
||||
(false, is_full)
|
||||
} else if last.is_some() {
|
||||
// 6. If the last argument is provided, I reverse the order of the results
|
||||
page.reverse();
|
||||
(is_full, false)
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
Ok((has_previous_page, has_next_page, page))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2021, 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.
|
||||
@ -31,7 +31,10 @@ use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||
use crate::Clock;
|
||||
use crate::{
|
||||
pagination::{generate_pagination, process_page},
|
||||
Clock,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UserLookup {
|
||||
@ -121,6 +124,7 @@ impl ActiveSessionLookupError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct SessionLookup {
|
||||
user_session_id: Uuid,
|
||||
user_id: Uuid,
|
||||
@ -223,6 +227,72 @@ pub async fn lookup_active_session(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
user.id = %user.data,
|
||||
user.username = user.username,
|
||||
),
|
||||
err(Display),
|
||||
)]
|
||||
pub async fn get_paginated_user_sessions(
|
||||
executor: impl PgExecutor<'_>,
|
||||
user: &User<PostgresqlBackend>,
|
||||
before: Option<Ulid>,
|
||||
after: Option<Ulid>,
|
||||
first: Option<usize>,
|
||||
last: Option<usize>,
|
||||
) -> Result<(bool, bool, Vec<BrowserSession<PostgresqlBackend>>), anyhow::Error> {
|
||||
let mut query = String::from(
|
||||
r#"
|
||||
SELECT
|
||||
s.user_session_id,
|
||||
u.user_id,
|
||||
u.username,
|
||||
s.created_at,
|
||||
a.user_session_authentication_id AS "last_authentication_id",
|
||||
a.created_at AS "last_authd_at",
|
||||
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 user_sessions s
|
||||
INNER JOIN users u
|
||||
USING (user_id)
|
||||
LEFT JOIN user_session_authentications a
|
||||
USING (user_session_id)
|
||||
LEFT JOIN user_emails ue
|
||||
ON ue.user_email_id = u.primary_user_email_id
|
||||
"#,
|
||||
);
|
||||
|
||||
let mut arguments = PgArguments::default();
|
||||
|
||||
query += " WHERE s.finished_at IS NULL AND s.user_id = ";
|
||||
arguments.add(Uuid::from(user.data));
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
|
||||
generate_pagination(
|
||||
&mut query,
|
||||
"s.user_session_id",
|
||||
&mut arguments,
|
||||
before,
|
||||
after,
|
||||
first,
|
||||
last,
|
||||
)?;
|
||||
|
||||
let page: Vec<SessionLookup> = sqlx::query_as_with(&query, arguments)
|
||||
.fetch_all(executor)
|
||||
.instrument(info_span!("Fetch paginated user emails", 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,
|
||||
fields(
|
||||
@ -681,8 +751,6 @@ pub async fn get_paginated_user_emails(
|
||||
first: Option<usize>,
|
||||
last: Option<usize>,
|
||||
) -> Result<(bool, bool, Vec<UserEmail<PostgresqlBackend>>), anyhow::Error> {
|
||||
// ref: https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564
|
||||
// 1. Start from the greedy query: SELECT * FROM table
|
||||
let mut query = String::from(
|
||||
r#"
|
||||
SELECT
|
||||
@ -700,64 +768,27 @@ pub async fn get_paginated_user_emails(
|
||||
arguments.add(Uuid::from(user.data));
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
|
||||
// 2. If the after argument is provided, add `id > parsed_cursor` to the `WHERE`
|
||||
// clause
|
||||
if let Some(after) = after {
|
||||
query += " AND ue.user_email_id > ";
|
||||
arguments.add(Uuid::from(after));
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
}
|
||||
generate_pagination(
|
||||
&mut query,
|
||||
"ue.user_email_id",
|
||||
&mut arguments,
|
||||
before,
|
||||
after,
|
||||
first,
|
||||
last,
|
||||
)?;
|
||||
|
||||
// 3. If the before argument is provided, add `id < parsed_cursor` to the
|
||||
// `WHERE` clause
|
||||
if let Some(before) = before {
|
||||
query += " AND ue.user_email_id < ";
|
||||
arguments.add(Uuid::from(before));
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
}
|
||||
|
||||
// 4. If the first argument is provided, add `ORDER BY id ASC LIMIT first+1` to
|
||||
// the query
|
||||
let limit = if let Some(count) = first {
|
||||
query += " ORDER BY ue.user_email_id ASC LIMIT ";
|
||||
arguments.add((count + 1) as i64);
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
count
|
||||
// 5. If the first argument is provided, add `ORDER BY id DESC LIMIT last+1`
|
||||
// to the query
|
||||
} else if let Some(count) = last {
|
||||
query += " ORDER BY ue.user_email_id DESC LIMIT ";
|
||||
arguments.add((count + 1) as i64);
|
||||
arguments.format_placeholder(&mut query)?;
|
||||
count
|
||||
} else {
|
||||
bail!("Either 'first' or 'last' must be specified");
|
||||
};
|
||||
|
||||
let mut res: Vec<UserEmailLookup> = sqlx::query_as_with(&query, arguments)
|
||||
let page: Vec<UserEmailLookup> = sqlx::query_as_with(&query, arguments)
|
||||
.fetch_all(executor)
|
||||
.instrument(info_span!("Fetch paginated user emails", query = query))
|
||||
.instrument(info_span!("Fetch paginated user sessions", query = query))
|
||||
.await?;
|
||||
|
||||
let is_full = res.len() == (limit + 1);
|
||||
if is_full {
|
||||
res.pop();
|
||||
}
|
||||
|
||||
let (has_previous_page, has_next_page) = if first.is_some() {
|
||||
(false, is_full)
|
||||
} else if last.is_some() {
|
||||
// 5. If the last argument is provided, I reverse the order of the results
|
||||
res.reverse();
|
||||
(is_full, false)
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
let (has_previous_page, has_next_page, page) = process_page(page, first, last)?;
|
||||
|
||||
Ok((
|
||||
has_previous_page,
|
||||
has_next_page,
|
||||
res.into_iter().map(Into::into).collect(),
|
||||
page.into_iter().map(Into::into).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user