fix(web): scope GET /api/v1/sessions to the authenticated user (#2445)

handle_list_all_sessions returned client_mgr.list_sessions(), which
iterates user_clients_map across ALL users and returns every session's
StorageToken (token, client_url, machine_id, user_id). The handler is
mounted under login_required! but performed no per-user authorization:
it fetched get_group_permissions() only to println! the result, then
returned the full cross-user list. Any authenticated user could read
every other user's device token and public client_url.

Scope the result to the caller by adding
Storage::list_user_client_tokens(user_id) /
ClientManager::list_sessions_by_user_id(user_id), mirroring the existing
per-user pattern in handle_get_summary (list_machine_by_user_id). Also
drop the leftover debug println! and the unwrap() on the current user
(return 401 instead).
This commit is contained in:
Moder Steven
2026-07-20 13:47:15 +08:00
committed by GitHub
parent f24735a86f
commit 346f32d3d0
3 changed files with 24 additions and 8 deletions
+4
View File
@@ -161,6 +161,10 @@ impl ClientManager {
self.storage.list_clients() self.storage.list_clients()
} }
pub async fn list_sessions_by_user_id(&self, user_id: UserIdInDb) -> Vec<StorageToken> {
self.storage.list_user_client_tokens(user_id)
}
pub async fn list_all_sessions(&self) -> Vec<StorageToken> { pub async fn list_all_sessions(&self) -> Vec<StorageToken> {
self.storage.list_all_clients() self.storage.list_all_clients()
} }
@@ -143,6 +143,21 @@ impl Storage {
self.list_clients_with_auth(true) self.list_clients_with_auth(true)
} }
/// List authorized client sessions that belong to a single user only.
pub fn list_user_client_tokens(&self, user_id: UserIdInDb) -> Vec<StorageToken> {
self.0
.user_clients_map
.get(&user_id)
.map(|info_map| {
info_map
.iter()
.filter(|info| info.value().authorized)
.map(|info| info.value().storage_token.clone())
.collect()
})
.unwrap_or_default()
}
pub fn list_all_clients(&self) -> Vec<StorageToken> { pub fn list_all_clients(&self) -> Vec<StorageToken> {
self.list_clients_with_auth(false) self.list_clients_with_auth(false)
} }
+5 -8
View File
@@ -14,7 +14,7 @@ use axum::response::Response;
use axum::routing::{delete, post}; use axum::routing::{delete, post};
use axum::{Extension, Json, Router, extract::State, routing::get}; use axum::{Extension, Json, Router, extract::State, routing::get};
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer}; use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
use axum_login::{AuthManagerLayerBuilder, AuthUser, AuthzBackend, login_required}; use axum_login::{AuthManagerLayerBuilder, AuthUser, login_required};
use axum_messages::MessagesManagerLayer; use axum_messages::MessagesManagerLayer;
use easytier::common::config::{ConfigLoader, TomlConfigLoader}; use easytier::common::config::{ConfigLoader, TomlConfigLoader};
use easytier::launcher::NetworkConfig; use easytier::launcher::NetworkConfig;
@@ -131,13 +131,10 @@ impl RestfulServer {
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> { ) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
let perms = auth_session let Some(user) = auth_session.user else {
.backend return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
.get_group_permissions(auth_session.user.as_ref().unwrap()) };
.await let ret = client_mgr.list_sessions_by_user_id(user.id()).await;
.unwrap();
println!("{:?}", perms);
let ret = client_mgr.list_sessions().await;
Ok(ListSessionJsonResp(ret).into()) Ok(ListSessionJsonResp(ret).into())
} }