refactor(dpop): apply clippy

This commit is contained in:
Edouard Vanbelle
2026-08-09 10:55:19 +02:00
parent 62475193ff
commit 6a434aebe6
6 changed files with 62 additions and 33 deletions
+24 -3
View File
@@ -21,6 +21,29 @@ use uuid::Uuid;
use crate::domain::entities::session::Session; use crate::domain::entities::session::Session;
/// Authenticated-caller context — the caller's identity + session-
/// bound signals a service method might key off. Constructed at the
/// handler boundary from `AuthUser` and passed through unchanged;
/// keeps service signatures flat instead of accumulating parallel
/// `caller_id`, `caller_jkt`, `caller_ip` parameters. Every
/// caller-context field lives here, one place to extend.
///
/// Not admin-specific — any handler that needs caller context can
/// build one from `AuthUser`. Admin methods just happen to be the
/// first callers (sessions panel's `is_current` comparison and
/// audit lines).
#[derive(Debug, Clone)]
pub struct SessionCaller<'a> {
/// AuthZ subject — used by `require_admin_caller` and audit lines.
pub id: Uuid,
/// Caller's own DPoP thumbprint from the JWT `cnf.jkt` claim.
/// Enables the sessions panel's "you are here" highlight
/// ([`SessionSummaryDto::is_current`]) — `None` when the caller
/// logged in via an unbound path (legacy password without DPoP,
/// pre-bind OIDC redirect, etc.).
pub dpop_jkt: Option<&'a str>,
}
/// Wire shape for `GET /api/admin/sessions`. Contains everything the /// Wire shape for `GET /api/admin/sessions`. Contains everything the
/// admin table renders and **nothing the raw session entity would /// admin table renders and **nothing the raw session entity would
/// leak** (refresh token, OIDC ID-token, full DPoP thumbprint). /// leak** (refresh token, OIDC ID-token, full DPoP thumbprint).
@@ -75,9 +98,7 @@ impl SessionSummaryDto {
let is_revoked = s.is_revoked(); let is_revoked = s.is_revoked();
let is_expired = s.is_expired(); let is_expired = s.is_expired();
let jkt = s.dpop_jkt().map(|s| s.to_owned()); let jkt = s.dpop_jkt().map(|s| s.to_owned());
let dpop_jkt_prefix = jkt let dpop_jkt_prefix = jkt.as_ref().map(|t| t.chars().take(8).collect::<String>());
.as_ref()
.map(|t| t.chars().take(8).collect::<String>());
let is_current = match (jkt.as_deref(), caller_jkt) { let is_current = match (jkt.as_deref(), caller_jkt) {
(Some(row), Some(caller)) => row == caller, (Some(row), Some(caller)) => row == caller,
_ => false, _ => false,
@@ -2962,14 +2962,13 @@ impl AuthApplicationService {
pub async fn admin_list_sessions_with_perms<A: AuthorizationEngine>( pub async fn admin_list_sessions_with_perms<A: AuthorizationEngine>(
&self, &self,
authorization: &A, authorization: &A,
caller_id: Uuid, caller: crate::application::dtos::session_dto::SessionCaller<'_>,
caller_dpop_jkt: Option<&str>,
user_id_filter: Option<Uuid>, user_id_filter: Option<Uuid>,
include_revoked: bool, include_revoked: bool,
limit: i64, limit: i64,
offset: i64, offset: i64,
) -> Result<Vec<crate::application::dtos::session_dto::SessionSummaryDto>, DomainError> { ) -> Result<Vec<crate::application::dtos::session_dto::SessionSummaryDto>, DomainError> {
self.require_admin_caller(authorization, caller_id).await?; self.require_admin_caller(authorization, caller.id).await?;
let sessions = self let sessions = self
.session_storage .session_storage
.list_sessions_paginated(user_id_filter, include_revoked, limit, offset) .list_sessions_paginated(user_id_filter, include_revoked, limit, offset)
@@ -2979,7 +2978,7 @@ impl AuthApplicationService {
.map(|s| { .map(|s| {
crate::application::dtos::session_dto::SessionSummaryDto::from_session( crate::application::dtos::session_dto::SessionSummaryDto::from_session(
s, s,
caller_dpop_jkt, caller.dpop_jkt,
) )
}) })
.collect()) .collect())
@@ -2994,10 +2993,10 @@ impl AuthApplicationService {
pub async fn admin_revoke_session_with_perms<A: AuthorizationEngine>( pub async fn admin_revoke_session_with_perms<A: AuthorizationEngine>(
&self, &self,
authorization: &A, authorization: &A,
caller_id: Uuid, caller: crate::application::dtos::session_dto::SessionCaller<'_>,
session_id: Uuid, session_id: Uuid,
) -> Result<(), DomainError> { ) -> Result<(), DomainError> {
self.require_admin_caller(authorization, caller_id).await?; self.require_admin_caller(authorization, caller.id).await?;
// Resolve target user for the audit line before revocation — // Resolve target user for the audit line before revocation —
// once the session row is revoked the user_id is still readable // once the session row is revoked the user_id is still readable
// but the ORDER is stable this way. // but the ORDER is stable this way.
@@ -3011,7 +3010,7 @@ impl AuthApplicationService {
tracing::info!( tracing::info!(
target: "audit", target: "audit",
event = "admin.session_revoked", event = "admin.session_revoked",
caller_id = %caller_id, caller_id = %caller.id,
session_id = %session_id, session_id = %session_id,
target_user_id = target_user_id.map(|u| u.to_string()).unwrap_or_default(), target_user_id = target_user_id.map(|u| u.to_string()).unwrap_or_default(),
"👮🏻‍♂️ Admin revoked session", "👮🏻‍♂️ Admin revoked session",
+20 -13
View File
@@ -18,9 +18,9 @@ use crate::application::dtos::plugin_dto::{
use crate::application::dtos::settings_dto::{ use crate::application::dtos::settings_dto::{
AdminCreateUserDto, AdminResetPasswordDto, DashboardStatsDto, DriveKindUsageDto, AdminCreateUserDto, AdminResetPasswordDto, DashboardStatsDto, DriveKindUsageDto,
ListSessionsQueryDto, ListUsersQueryDto, MigrationStateDto, SaveOidcSettingsDto, ListSessionsQueryDto, ListUsersQueryDto, MigrationStateDto, SaveOidcSettingsDto,
SaveStorageSettingsDto, SaveStorageSettingsDto, SendSmtpTestDto, SmtpInfoDto, SmtpTestResultDto, StartMigrationDto,
SendSmtpTestDto, SmtpInfoDto, SmtpTestResultDto, StartMigrationDto, TestOidcConnectionDto, TestOidcConnectionDto, TestStorageConnectionDto, UpdateUserActiveDto, UpdateUserQuotaDto,
TestStorageConnectionDto, UpdateUserActiveDto, UpdateUserQuotaDto, UpdateUserRoleDto, UpdateUserRoleDto,
}; };
use crate::application::dtos::user_dto::{AdminUserSummaryDto, UserDto}; use crate::application::dtos::user_dto::{AdminUserSummaryDto, UserDto};
use crate::application::ports::authorization_ports::AuthorizationEngine; use crate::application::ports::authorization_ports::AuthorizationEngine;
@@ -1238,20 +1238,23 @@ pub async fn list_sessions(
None => None, None => None,
}; };
// Pass the caller's DPoP thumbprint so the DTO can flag which // Pass the caller's DPoP thumbprint through the SessionCaller
// row is the admin's own current session (`is_current = true`). // wrapper so the DTO can flag which row is the admin's own
// Rendered as a "this is you" badge — prevents the admin from // current session (`is_current = true`). Rendered as a "this
// accidentally revoking the session they're clicking from. // is you" badge — prevents accidentally revoking the session
// `None` when the admin is unbound (rare — legacy / migration // the click came from. `None` when the admin is unbound (rare
// window sessions), in which case no row highlights. // — legacy / migration-window sessions), in which case no row
let caller_jkt = auth_user.dpop_jkt.as_deref(); // highlights.
let caller = crate::application::dtos::session_dto::SessionCaller {
id: auth_user.id,
dpop_jkt: auth_user.dpop_jkt.as_deref(),
};
let sessions = auth let sessions = auth
.auth_application_service .auth_application_service
.admin_list_sessions_with_perms( .admin_list_sessions_with_perms(
state.authorization.as_ref(), state.authorization.as_ref(),
auth_user.id, caller,
caller_jkt,
user_id_filter, user_id_filter,
include_revoked, include_revoked,
limit, limit,
@@ -1293,8 +1296,12 @@ pub async fn revoke_session(
.as_ref() .as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?; .ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
let caller = crate::application::dtos::session_dto::SessionCaller {
id: auth_user.id,
dpop_jkt: auth_user.dpop_jkt.as_deref(),
};
auth.auth_application_service auth.auth_application_service
.admin_revoke_session_with_perms(state.authorization.as_ref(), auth_user.id, session_id) .admin_revoke_session_with_perms(state.authorization.as_ref(), caller, session_id)
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
@@ -770,8 +770,11 @@ pub async fn login_ke3(
// `user_agent` land populated instead of NULL (admin panel would // `user_agent` land populated instead of NULL (admin panel would
// otherwise render "—"). Both are per-session and only refresh // otherwise render "—"). Both are per-session and only refresh
// on rotation, matching the login pattern. // on rotation, matching the login pattern.
let client_ip = let client_ip = crate::interfaces::middleware::trusted_proxy::client_ip_from_parts(
crate::interfaces::middleware::trusted_proxy::client_ip_from_parts(&headers, Some(peer), false); &headers,
Some(peer),
false,
);
let user_agent = headers let user_agent = headers
.get(axum::http::header::USER_AGENT) .get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
-1
View File
@@ -412,5 +412,4 @@ mod tests {
("http".to_owned(), "localhost".to_owned()) ("http".to_owned(), "localhost".to_owned())
); );
} }
} }
+7 -7
View File
@@ -1396,13 +1396,13 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
// Installs the recorder BEFORE the main listener starts serving so // Installs the recorder BEFORE the main listener starts serving so
// the first request's counter increments are captured (recorder // the first request's counter increments are captured (recorder
// install is racy vs first emit — order matters). // install is racy vs first emit — order matters).
if let Some(metrics_addr) = config.metrics_listen { if let Some(metrics_addr) = config.metrics_listen
if let Err(err) = oxicloud::interfaces::metrics::spawn(metrics_addr).await { && let Err(err) = oxicloud::interfaces::metrics::spawn(metrics_addr).await
// Fail loudly: operators asked for metrics; not surfacing {
// this would hide a misconfigured scrape endpoint. // Fail loudly: operators asked for metrics; not surfacing
tracing::error!("Prometheus /metrics setup failed: {err}"); // this would hide a misconfigured scrape endpoint.
return Err(err); tracing::error!("Prometheus /metrics setup failed: {err}");
} return Err(err);
} }
let socket = make_socket(&addr, reuse_port)?; let socket = make_socket(&addr, reuse_port)?;