Add filters for personal sessions
This commit is contained in:
@@ -108,6 +108,21 @@ pub enum OAuth2Clients {
|
|||||||
IsStatic,
|
IsStatic,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sea_query::Iden)]
|
||||||
|
#[iden = "personal_sessions"]
|
||||||
|
pub enum PersonalSessions {
|
||||||
|
Table,
|
||||||
|
PersonalSessionId,
|
||||||
|
OwnerUserId,
|
||||||
|
ActorUserId,
|
||||||
|
HumanName,
|
||||||
|
ScopeList,
|
||||||
|
CreatedAt,
|
||||||
|
RevokedAt,
|
||||||
|
LastActiveAt,
|
||||||
|
LastActiveIp,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sea_query::Iden)]
|
#[derive(sea_query::Iden)]
|
||||||
#[iden = "upstream_oauth_providers"]
|
#[iden = "upstream_oauth_providers"]
|
||||||
pub enum UpstreamOAuthProviders {
|
pub enum UpstreamOAuthProviders {
|
||||||
|
|||||||
@@ -11,14 +11,29 @@ use mas_data_model::{
|
|||||||
Clock, User,
|
Clock, User,
|
||||||
personal::session::{PersonalSession, SessionState},
|
personal::session::{PersonalSession, SessionState},
|
||||||
};
|
};
|
||||||
use mas_storage::personal::PersonalSessionRepository;
|
use mas_storage::{
|
||||||
|
Page, Pagination,
|
||||||
|
personal::{PersonalSessionFilter, PersonalSessionRepository, PersonalSessionState},
|
||||||
|
};
|
||||||
use oauth2_types::scope::Scope;
|
use oauth2_types::scope::Scope;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use sea_query::{
|
||||||
|
Condition, Expr, PgFunc, PostgresQueryBuilder, Query, SimpleExpr, enum_def,
|
||||||
|
extension::postgres::PgExpr as _,
|
||||||
|
};
|
||||||
|
use sea_query_binder::SqlxBinder as _;
|
||||||
use sqlx::PgConnection;
|
use sqlx::PgConnection;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{DatabaseError, errors::DatabaseInconsistencyError, tracing::ExecuteExt as _};
|
use crate::{
|
||||||
|
DatabaseError,
|
||||||
|
errors::DatabaseInconsistencyError,
|
||||||
|
filter::{Filter, StatementExt as _},
|
||||||
|
iden::PersonalSessions,
|
||||||
|
pagination::QueryBuilderExt as _,
|
||||||
|
tracing::ExecuteExt as _,
|
||||||
|
};
|
||||||
|
|
||||||
/// An implementation of [`PersonalSessionRepository`] for a PostgreSQL
|
/// An implementation of [`PersonalSessionRepository`] for a PostgreSQL
|
||||||
/// connection
|
/// connection
|
||||||
@@ -27,13 +42,15 @@ pub struct PgPersonalSessionRepository<'c> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'c> PgPersonalSessionRepository<'c> {
|
impl<'c> PgPersonalSessionRepository<'c> {
|
||||||
/// Create a new [`PgOAuth2SessionRepository`] from an active PostgreSQL
|
/// Create a new [`PgPersonalSessionRepository`] from an active PostgreSQL
|
||||||
/// connection
|
/// connection
|
||||||
pub fn new(conn: &'c mut PgConnection) -> Self {
|
pub fn new(conn: &'c mut PgConnection) -> Self {
|
||||||
Self { conn }
|
Self { conn }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
#[enum_def]
|
||||||
struct PersonalSessionLookup {
|
struct PersonalSessionLookup {
|
||||||
personal_session_id: Uuid,
|
personal_session_id: Uuid,
|
||||||
owner_user_id: Uuid,
|
owner_user_id: Uuid,
|
||||||
@@ -215,4 +232,151 @@ impl PersonalSessionRepository for PgPersonalSessionRepository<'_> {
|
|||||||
.finish(finished_at)
|
.finish(finished_at)
|
||||||
.map_err(DatabaseError::to_invalid_operation)
|
.map_err(DatabaseError::to_invalid_operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "db.personal_session.list",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
db.query.text,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
async fn list(
|
||||||
|
&mut self,
|
||||||
|
filter: PersonalSessionFilter<'_>,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<PersonalSession>, Self::Error> {
|
||||||
|
let (sql, arguments) = Query::select()
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)),
|
||||||
|
PersonalSessionLookupIden::PersonalSessionId,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::OwnerUserId)),
|
||||||
|
PersonalSessionLookupIden::OwnerUserId,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::ActorUserId)),
|
||||||
|
PersonalSessionLookupIden::ActorUserId,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::HumanName)),
|
||||||
|
PersonalSessionLookupIden::HumanName,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)),
|
||||||
|
PersonalSessionLookupIden::ScopeList,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::CreatedAt)),
|
||||||
|
PersonalSessionLookupIden::CreatedAt,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)),
|
||||||
|
PersonalSessionLookupIden::RevokedAt,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt)),
|
||||||
|
PersonalSessionLookupIden::LastActiveAt,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveIp)),
|
||||||
|
PersonalSessionLookupIden::LastActiveIp,
|
||||||
|
)
|
||||||
|
.from(PersonalSessions::Table)
|
||||||
|
.apply_filter(filter)
|
||||||
|
.generate_pagination(
|
||||||
|
(PersonalSessions::Table, PersonalSessions::PersonalSessionId),
|
||||||
|
pagination,
|
||||||
|
)
|
||||||
|
.build_sqlx(PostgresQueryBuilder);
|
||||||
|
|
||||||
|
let edges: Vec<PersonalSessionLookup> = sqlx::query_as_with(&sql, arguments)
|
||||||
|
.traced()
|
||||||
|
.fetch_all(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let page = pagination
|
||||||
|
.process(edges)
|
||||||
|
.try_map(PersonalSession::try_from)?;
|
||||||
|
|
||||||
|
Ok(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "db.personal_session.count",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
db.query.text,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error> {
|
||||||
|
let (sql, arguments) = Query::select()
|
||||||
|
.expr(Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)).count())
|
||||||
|
.from(PersonalSessions::Table)
|
||||||
|
.apply_filter(filter)
|
||||||
|
.build_sqlx(PostgresQueryBuilder);
|
||||||
|
|
||||||
|
let count: i64 = sqlx::query_scalar_with(&sql, arguments)
|
||||||
|
.traced()
|
||||||
|
.fetch_one(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
count
|
||||||
|
.try_into()
|
||||||
|
.map_err(DatabaseError::to_invalid_operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter for PersonalSessionFilter<'_> {
|
||||||
|
fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
|
||||||
|
sea_query::Condition::all()
|
||||||
|
.add_option(self.owner_user().map(|user| {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::OwnerUserId))
|
||||||
|
.eq(Uuid::from(user.id))
|
||||||
|
}))
|
||||||
|
.add_option(self.actor_user().map(|user| {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::ActorUserId))
|
||||||
|
.eq(Uuid::from(user.id))
|
||||||
|
}))
|
||||||
|
.add_option(self.device().map(|device| -> SimpleExpr {
|
||||||
|
if let Ok([stable_scope_token, unstable_scope_token]) = device.to_scope_token() {
|
||||||
|
Condition::any()
|
||||||
|
.add(
|
||||||
|
Expr::val(stable_scope_token.to_string()).eq(PgFunc::any(Expr::col((
|
||||||
|
PersonalSessions::Table,
|
||||||
|
PersonalSessions::ScopeList,
|
||||||
|
)))),
|
||||||
|
)
|
||||||
|
.add(Expr::val(unstable_scope_token.to_string()).eq(PgFunc::any(
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)),
|
||||||
|
)))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
// If the device ID can't be encoded as a scope token, match no rows
|
||||||
|
Expr::val(false).into()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.add_option(self.state().map(|state| match state {
|
||||||
|
PersonalSessionState::Active => {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)).is_null()
|
||||||
|
}
|
||||||
|
PersonalSessionState::Revoked => {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)).is_not_null()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.add_option(self.scope().map(|scope| {
|
||||||
|
let scope: Vec<String> = scope.iter().map(|s| s.as_str().to_owned()).collect();
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)).contains(scope)
|
||||||
|
}))
|
||||||
|
.add_option(self.last_active_before().map(|last_active_before| {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt))
|
||||||
|
.lt(last_active_before)
|
||||||
|
}))
|
||||||
|
.add_option(self.last_active_after().map(|last_active_after| {
|
||||||
|
Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt))
|
||||||
|
.gt(last_active_after)
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,7 @@
|
|||||||
mod access_token;
|
mod access_token;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
pub use self::{access_token::PersonalAccessTokenRepository, session::PersonalSessionRepository};
|
pub use self::{
|
||||||
|
access_token::PersonalAccessTokenRepository,
|
||||||
|
session::{PersonalSessionFilter, PersonalSessionRepository, PersonalSessionState},
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use oauth2_types::scope::Scope;
|
|||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::repository_impl;
|
use crate::{Page, Pagination, repository_impl};
|
||||||
|
|
||||||
/// A [`PersonalSessionRepository`] helps interacting with
|
/// A [`PersonalSessionRepository`] helps interacting with
|
||||||
/// [`PersonalSession`] saved in the storage backend
|
/// [`PersonalSession`] saved in the storage backend
|
||||||
@@ -78,6 +78,34 @@ pub trait PersonalSessionRepository: Send + Sync {
|
|||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
personal_session: PersonalSession,
|
personal_session: PersonalSession,
|
||||||
) -> Result<PersonalSession, Self::Error>;
|
) -> Result<PersonalSession, Self::Error>;
|
||||||
|
|
||||||
|
/// List [`PersonalSession`]s matching the given filter and pagination
|
||||||
|
/// parameters
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `filter`: The filter parameters
|
||||||
|
/// * `pagination`: The pagination parameters
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
|
async fn list(
|
||||||
|
&mut self,
|
||||||
|
filter: PersonalSessionFilter<'_>,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<PersonalSession>, Self::Error>;
|
||||||
|
|
||||||
|
/// Count [`PersonalSession`]s matching the given filter
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `filter`: The filter parameters
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
|
async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
repository_impl!(PersonalSessionRepository:
|
repository_impl!(PersonalSessionRepository:
|
||||||
@@ -98,4 +126,155 @@ repository_impl!(PersonalSessionRepository:
|
|||||||
clock: &dyn Clock,
|
clock: &dyn Clock,
|
||||||
personal_session: PersonalSession,
|
personal_session: PersonalSession,
|
||||||
) -> Result<PersonalSession, Self::Error>;
|
) -> Result<PersonalSession, Self::Error>;
|
||||||
|
|
||||||
|
async fn list(
|
||||||
|
&mut self,
|
||||||
|
filter: PersonalSessionFilter<'_>,
|
||||||
|
pagination: Pagination,
|
||||||
|
) -> Result<Page<PersonalSession>, Self::Error>;
|
||||||
|
|
||||||
|
async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Filter parameters for listing personal sessions
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct PersonalSessionFilter<'a> {
|
||||||
|
owner_user: Option<&'a User>,
|
||||||
|
actor_user: Option<&'a User>,
|
||||||
|
device: Option<&'a Device>,
|
||||||
|
state: Option<PersonalSessionState>,
|
||||||
|
scope: Option<&'a Scope>,
|
||||||
|
last_active_before: Option<DateTime<Utc>>,
|
||||||
|
last_active_after: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter for what state a personal session is in.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PersonalSessionState {
|
||||||
|
/// The personal session is active, which means it either
|
||||||
|
/// has active access tokens or can have new access tokens generated.
|
||||||
|
Active,
|
||||||
|
/// The personal session is revoked, which means no more access tokens
|
||||||
|
/// can be generated and none are active.
|
||||||
|
Revoked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PersonalSessionFilter<'a> {
|
||||||
|
/// Create a new [`PersonalSessionFilter`] with default values
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List sessions owned by a specific user
|
||||||
|
#[must_use]
|
||||||
|
pub fn for_owner_user(mut self, user: &'a User) -> Self {
|
||||||
|
self.owner_user = Some(user);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the owner user filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no user filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn owner_user(&self) -> Option<&'a User> {
|
||||||
|
self.owner_user
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List sessions acting as a specific user
|
||||||
|
#[must_use]
|
||||||
|
pub fn for_actor_user(mut self, user: &'a User) -> Self {
|
||||||
|
self.actor_user = Some(user);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the actor user filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no user filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn actor_user(&self) -> Option<&'a User> {
|
||||||
|
self.actor_user
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return sessions with a last active time before the given time
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
|
||||||
|
self.last_active_before = Some(last_active_before);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return sessions with a last active time after the given time
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
|
||||||
|
self.last_active_after = Some(last_active_after);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the last active before filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no client filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.last_active_before
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the last active after filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no client filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.last_active_after
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return active sessions
|
||||||
|
#[must_use]
|
||||||
|
pub fn active_only(mut self) -> Self {
|
||||||
|
self.state = Some(PersonalSessionState::Active);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return finished sessions
|
||||||
|
#[must_use]
|
||||||
|
pub fn finished_only(mut self) -> Self {
|
||||||
|
self.state = Some(PersonalSessionState::Revoked);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no state filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn state(&self) -> Option<PersonalSessionState> {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return sessions with the given scope
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_scope(mut self, scope: &'a Scope) -> Self {
|
||||||
|
self.scope = Some(scope);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scope filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no scope filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn scope(&self) -> Option<&'a Scope> {
|
||||||
|
self.scope
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only return sessions that have the given device in their scope
|
||||||
|
#[must_use]
|
||||||
|
pub fn for_device(mut self, device: &'a Device) -> Self {
|
||||||
|
self.device = Some(device);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the device filter
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no device filter was set
|
||||||
|
#[must_use]
|
||||||
|
pub fn device(&self) -> Option<&'a Device> {
|
||||||
|
self.device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user