Add scope filter to personal sessions list

This commit is contained in:
Olivier 'reivilibre
2025-10-21 11:02:59 +01:00
parent a7a29cfd32
commit 9c88510540

View File

@@ -3,17 +3,17 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
use std::str::FromStr as _;
use aide::{OperationIo, transform::TransformOperation}; use aide::{OperationIo, transform::TransformOperation};
use axum::{ use axum::{Json, response::IntoResponse};
Json, use axum_extra::extract::{Query, QueryRejection};
extract::{Query, rejection::QueryRejection},
response::IntoResponse,
};
use axum_macros::FromRequestParts; use axum_macros::FromRequestParts;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyper::StatusCode; use hyper::StatusCode;
use mas_axum_utils::record_error; use mas_axum_utils::record_error;
use mas_storage::personal::PersonalSessionFilter; use mas_storage::personal::PersonalSessionFilter;
use oauth2_types::scope::{Scope, ScopeToken};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use ulid::Ulid; use ulid::Ulid;
@@ -64,6 +64,10 @@ pub struct FilterParams {
#[schemars(with = "Option<crate::admin::schema::Ulid>")] #[schemars(with = "Option<crate::admin::schema::Ulid>")]
actor_user: Option<Ulid>, actor_user: Option<Ulid>,
/// Retrieve the items with the given scope
#[serde(default, rename = "filter[scope]")]
scope: Vec<String>,
/// Filter by session status /// Filter by session status
#[serde(rename = "filter[status]")] #[serde(rename = "filter[status]")]
status: Option<PersonalSessionStatus>, status: Option<PersonalSessionStatus>,
@@ -97,6 +101,10 @@ impl std::fmt::Display for FilterParams {
write!(f, "{sep}filter[actor_user]={actor_user}")?; write!(f, "{sep}filter[actor_user]={actor_user}")?;
sep = '&'; sep = '&';
} }
for scope in &self.scope {
write!(f, "{sep}filter[scope]={scope}")?;
sep = '&';
}
if let Some(status) = self.status { if let Some(status) = self.status {
write!(f, "{sep}filter[status]={status}")?; write!(f, "{sep}filter[status]={status}")?;
sep = '&'; sep = '&';
@@ -141,6 +149,9 @@ pub enum RouteError {
#[error("Invalid filter parameters")] #[error("Invalid filter parameters")]
InvalidFilter(#[from] QueryRejection), InvalidFilter(#[from] QueryRejection),
#[error("Invalid scope {0:?} in filter parameters")]
InvalidScope(String),
} }
impl_from_error_for_route!(mas_storage::RepositoryError); impl_from_error_for_route!(mas_storage::RepositoryError);
@@ -153,7 +164,7 @@ impl IntoResponse for RouteError {
let status = match self { let status = match self {
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::UserNotFound(_) | Self::ClientNotFound(_) => StatusCode::NOT_FOUND, Self::UserNotFound(_) | Self::ClientNotFound(_) => StatusCode::NOT_FOUND,
Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, Self::InvalidScope(_) | Self::InvalidFilter(_) => StatusCode::BAD_REQUEST,
}; };
(status, sentry_event_id, Json(error)).into_response() (status, sentry_event_id, Json(error)).into_response()
} }
@@ -259,7 +270,18 @@ pub async fn handler(
None => filter, None => filter,
}; };
// Apply status filter let scope: Scope = params
.scope
.into_iter()
.map(|s| ScopeToken::from_str(&s).map_err(|_| RouteError::InvalidScope(s)))
.collect::<Result<_, _>>()?;
let filter = if scope.is_empty() {
filter
} else {
filter.with_scope(&scope)
};
let filter = match params.status { let filter = match params.status {
Some(PersonalSessionStatus::Active) => filter.active_only(), Some(PersonalSessionStatus::Active) => filter.active_only(),
Some(PersonalSessionStatus::Revoked) => filter.finished_only(), Some(PersonalSessionStatus::Revoked) => filter.finished_only(),
@@ -402,7 +424,7 @@ mod tests {
PersonalSessionOwner::from(&user), PersonalSessionOwner::from(&user),
&user, &user,
"Another test session".to_owned(), "Another test session".to_owned(),
Scope::from_iter([OPENID]), Scope::from_iter([OPENID, "urn:mas:admin".parse().unwrap()]),
) )
.await .await
.unwrap(); .unwrap();
@@ -490,7 +512,7 @@ mod tests {
"owner_client_id": null, "owner_client_id": null,
"actor_user_id": "01FSHN9AG09FE39KETP6F390F8", "actor_user_id": "01FSHN9AG09FE39KETP6F390F8",
"human_name": "Another test session", "human_name": "Another test session",
"scope": "openid", "scope": "openid urn:mas:admin",
"last_active_at": null, "last_active_at": null,
"last_active_ip": null, "last_active_ip": null,
"expires_at": "2022-02-01T14:40:00Z" "expires_at": "2022-02-01T14:40:00Z"
@@ -533,6 +555,10 @@ mod tests {
&["01FSHN9AG0YQYAR04VCYTHJ8SK", "01FSPT2RG08Y11Y5BM4VZ4CN8K"], &["01FSHN9AG0YQYAR04VCYTHJ8SK", "01FSPT2RG08Y11Y5BM4VZ4CN8K"],
), ),
("filter[expires]=false", &["01FSM7P1G0VBGAMK9D9QMGQ5MY"]), ("filter[expires]=false", &["01FSM7P1G0VBGAMK9D9QMGQ5MY"]),
(
"filter[scope]=urn:mas:admin",
&["01FSPT2RG08Y11Y5BM4VZ4CN8K"],
),
]; ];
for (filter, expected_ids) in filters_and_expected { for (filter, expected_ids) in filters_and_expected {