admin-api: filter user list by presence of active compat session

Adds `filter[has-active-compat-session]=true|false` to
`GET /api/admin/v1/users`. When `true` (resp. `false`), only users with
(resp. without) at least one active (non-finished) compatibility session
are returned.
This commit is contained in:
Quentin Gliech committed 2026-06-01 17:30:14 +02:00
1 parent be96582e9e
commit ee9a7ccaef
3 files changed
+108 -1

No files matched your search

@@ -85,6 +85,11 @@ pub struct FilterParams {
#[serde(default, rename = "filter[active-oauth2-client]")]
#[schemars(with = "Vec<crate::admin::schema::Ulid>")]
active_oauth2_client: Vec<Ulid>,
/// Retrieve users which have (or don't have) at least one active
/// (non-finished) compatibility session.
#[serde(rename = "filter[has-active-compat-session]")]
has_active_compat_session: Option<bool>,
}
impl std::fmt::Display for FilterParams {
@@ -111,6 +116,10 @@ impl std::fmt::Display for FilterParams {
write!(f, "{sep}filter[active-oauth2-client]={client}")?;
sep = '&';
}
if let Some(has) = self.has_active_compat_session {
write!(f, "{sep}filter[has-active-compat-session]={has}")?;
sep = '&';
}
let _ = sep;
Ok(())
@@ -228,6 +237,11 @@ pub async fn handler(
filter.with_active_oauth2_session_for_any_of_clients(&params.active_oauth2_client)
};
let filter = match params.has_active_compat_session {
Some(has) => filter.with_active_compat_session(has),
None => filter,
};
let response = match include_count {
IncludeCount::True => {
let page = repo.user().list(filter, pagination).await?;
@@ -250,8 +264,10 @@ pub async fn handler(
#[cfg(test)]
mod tests {
use hyper::{Request, StatusCode};
use mas_data_model::Device;
use mas_storage::{
RepositoryAccess,
compat::CompatSessionRepository,
oauth2::{OAuth2ClientRepository, OAuth2SessionRepository},
};
use oauth2_types::{
@@ -612,4 +628,75 @@ mod tests {
let response = state.request(request).await;
response.assert_status(StatusCode::NOT_FOUND);
}
/// Test the `filter[has-active-compat-session]` filter in both
/// directions.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_list_users_filter_has_active_compat_session(pool: PgPool) {
setup();
let mut state = TestState::from_pool(pool).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;
let mut rng = state.rng();
let mut repo = state.repository().await.unwrap();
// Alice has an active compat session.
let alice = repo
.user()
.add(&mut rng, &state.clock, "alice".to_owned())
.await
.unwrap();
let device = Device::generate(&mut rng);
repo.compat_session()
.add(&mut rng, &state.clock, &alice, device, None, false, None)
.await
.unwrap();
// Bob has a finished compat session.
let bob = repo
.user()
.add(&mut rng, &state.clock, "bob".to_owned())
.await
.unwrap();
let device = Device::generate(&mut rng);
let bob_session = repo
.compat_session()
.add(&mut rng, &state.clock, &bob, device, None, false, None)
.await
.unwrap();
repo.compat_session()
.finish(&state.clock, bob_session)
.await
.unwrap();
// Carol has no compat session.
repo.user()
.add(&mut rng, &state.clock, "carol".to_owned())
.await
.unwrap();
repo.save().await.unwrap();
// has-active-compat-session=true -> 1 (alice)
let request =
Request::get("/api/admin/v1/users?count=only&filter[has-active-compat-session]=true")
.bearer(&token)
.empty();
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(body["meta"]["count"], 1);
// has-active-compat-session=false -> 2 (bob + carol). The admin
// token is created via client-credentials and does not provision a
// user.
let request =
Request::get("/api/admin/v1/users?count=only&filter[has-active-compat-session]=false")
.bearer(&token)
.empty();
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(body["meta"]["count"], 2);
}
}
+1 -1
View File
@@ -841,7 +841,7 @@ async fn test_user_session(pool: PgPool) {
/// Test [`UserFilter::with_active_oauth2_session_for_any_of_clients`].
///
/// Sets up three users (A, B, C) with various OAuth2 session states across
/// Sets up three users (A, B, C) with various `OAuth2` session states across
/// three clients, and asserts that filtering on `[client_x, client_y]`
/// returns users that have an ACTIVE session for *any* of those clients
/// (OR semantics).
+20
View File
@@ -1966,6 +1966,19 @@
"default": []
},
"style": "form"
},
{
"in": "query",
"name": "filter[has-active-compat-session]",
"description": "Retrieve users which have (or don't have) at least one active\n (non-finished) compatibility session.",
"schema": {
"description": "Retrieve users which have (or don't have) at least one active\n (non-finished) compatibility session.",
"type": [
"boolean",
"null"
]
},
"style": "form"
}
],
"responses": {
@@ -6183,6 +6196,13 @@
"$ref": "#/components/schemas/ULID"
},
"default": []
},
"filter[has-active-compat-session]": {
"description": "Retrieve users which have (or don't have) at least one active\n (non-finished) compatibility session.",
"type": [
"boolean",
"null"
]
}
}
},