From bfe2d79a59ab4629bdaf041e3b1abc065544126e Mon Sep 17 00:00:00 2001 From: restitux Date: Thu, 16 Apr 2026 02:35:05 +0000 Subject: [PATCH] backend: gate existing endpoints behind auth and app permissions Move /api/pair, /api/apps, and /api/stream/start under the session auth middleware so they require a valid session token. Add app-level permission filtering: non-admin users only see and can stream apps they have been explicitly granted access to. Admins bypass all permission checks. Co-Authored-By: Claude Opus 4.6 --- gamestream-webtransport-proxy/src/apps.rs | 19 ++++++++++++++++++- gamestream-webtransport-proxy/src/main.rs | 7 +++---- gamestream-webtransport-proxy/src/stream.rs | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/gamestream-webtransport-proxy/src/apps.rs b/gamestream-webtransport-proxy/src/apps.rs index 8776ccb..fa59230 100644 --- a/gamestream-webtransport-proxy/src/apps.rs +++ b/gamestream-webtransport-proxy/src/apps.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, error}; use crate::{ + auth, common, common::{AppError, AppResult}, responses, @@ -45,7 +46,8 @@ struct GetAppsResponse { #[craft] impl crate::backend::Backend { #[craft(endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR)))] - pub async fn get_apps(self: ::std::sync::Arc) -> AppResult> { + pub async fn get_apps(self: ::std::sync::Arc, depot: &mut Depot) -> AppResult> { + let user = auth::get_user_from_depot(depot).cloned(); let standard_error = Err(AppError { status_code: StatusCode::INTERNAL_SERVER_ERROR, description: "failed to get available apps".to_string(), @@ -143,6 +145,21 @@ impl crate::backend::Backend { get_apps_resp.apps.insert(server.name, resp_vec); } + // Filter apps by user permissions (admins see everything) + if let Some(ref user) = user { + if !user.is_admin { + let permissions = self.db.get_permissions(&user.id).unwrap_or_default(); + for (server_name, apps) in get_apps_resp.apps.iter_mut() { + apps.retain(|app| { + permissions.iter().any(|p| { + p.server == *server_name && p.app_id == app.id as i64 + }) + }); + } + get_apps_resp.apps.retain(|_, apps| !apps.is_empty()); + } + } + Ok(Json(get_apps_resp)) } } diff --git a/gamestream-webtransport-proxy/src/main.rs b/gamestream-webtransport-proxy/src/main.rs index d3e1161..7b94830 100644 --- a/gamestream-webtransport-proxy/src/main.rs +++ b/gamestream-webtransport-proxy/src/main.rs @@ -76,16 +76,15 @@ async fn run_backend(port: u16) -> Result<()> { let router = Router::new() // Public auth routes .push(Router::with_path("api/auth/login").post(backend_arc.login())) - // Existing routes (not yet gated - will be gated in a subsequent commit) - .push(Router::with_path("api/pair").post(backend_arc.post_pair())) - .push(Router::with_path("api/apps").get(backend_arc.get_apps())) - .push(Router::with_path("api/stream/start").post(backend_arc.post_stream_start())) // Authenticated routes .push( Router::with_path("api") .hoop(auth_middleware) .push(Router::with_path("auth/logout").post(backend_arc.logout())) .push(Router::with_path("auth/me").get(backend_arc.me())) + .push(Router::with_path("pair").post(backend_arc.post_pair())) + .push(Router::with_path("apps").get(backend_arc.get_apps())) + .push(Router::with_path("stream/start").post(backend_arc.post_stream_start())) // Admin-only routes .push( Router::with_path("admin") diff --git a/gamestream-webtransport-proxy/src/stream.rs b/gamestream-webtransport-proxy/src/stream.rs index c1a9b79..196dfc5 100644 --- a/gamestream-webtransport-proxy/src/stream.rs +++ b/gamestream-webtransport-proxy/src/stream.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, error, info}; use crate::{ + auth, common::{AppError, AppResult, get_url}, proxy, responses, state::{GamestreamServer, StateReadAccess, StateReader}, @@ -81,12 +82,32 @@ impl crate::backend::Backend { self: ::std::sync::Arc, body: salvo::oapi::extract::JsonBody, req: &mut Request, + depot: &mut Depot, ) -> AppResult> { let standard_error = Err(crate::common::AppError { status_code: StatusCode::INTERNAL_SERVER_ERROR, description: "Could not start stream".to_string(), }); + // Check app permission + if let Some(user) = auth::get_user_from_depot(depot) { + if !user.is_admin { + match self.db.check_app_permission(&user.id, &body.server, body.id as i64) { + Ok(true) => {} + Ok(false) => { + return Err(AppError { + status_code: StatusCode::FORBIDDEN, + description: "You do not have permission to access this application".to_string(), + }); + } + Err(e) => { + error!("Permission check error: {e}"); + return standard_error; + } + } + } + } + let reader = self.state.read().await; let server = match get_server(&reader, &body.server) {