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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
auth,
|
||||||
common,
|
common,
|
||||||
common::{AppError, AppResult},
|
common::{AppError, AppResult},
|
||||||
responses,
|
responses,
|
||||||
@@ -45,7 +46,8 @@ struct GetAppsResponse {
|
|||||||
#[craft]
|
#[craft]
|
||||||
impl crate::backend::Backend {
|
impl crate::backend::Backend {
|
||||||
#[craft(endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR)))]
|
#[craft(endpoint(status_codes(StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR)))]
|
||||||
pub async fn get_apps(self: ::std::sync::Arc<Self>) -> AppResult<Json<GetAppsResponse>> {
|
pub async fn get_apps(self: ::std::sync::Arc<Self>, depot: &mut Depot) -> AppResult<Json<GetAppsResponse>> {
|
||||||
|
let user = auth::get_user_from_depot(depot).cloned();
|
||||||
let standard_error = Err(AppError {
|
let standard_error = Err(AppError {
|
||||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
description: "failed to get available apps".to_string(),
|
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);
|
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))
|
Ok(Json(get_apps_resp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,16 +76,15 @@ async fn run_backend(port: u16) -> Result<()> {
|
|||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
// Public auth routes
|
// Public auth routes
|
||||||
.push(Router::with_path("api/auth/login").post(backend_arc.login()))
|
.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
|
// Authenticated routes
|
||||||
.push(
|
.push(
|
||||||
Router::with_path("api")
|
Router::with_path("api")
|
||||||
.hoop(auth_middleware)
|
.hoop(auth_middleware)
|
||||||
.push(Router::with_path("auth/logout").post(backend_arc.logout()))
|
.push(Router::with_path("auth/logout").post(backend_arc.logout()))
|
||||||
.push(Router::with_path("auth/me").get(backend_arc.me()))
|
.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
|
// Admin-only routes
|
||||||
.push(
|
.push(
|
||||||
Router::with_path("admin")
|
Router::with_path("admin")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
auth,
|
||||||
common::{AppError, AppResult, get_url},
|
common::{AppError, AppResult, get_url},
|
||||||
proxy, responses,
|
proxy, responses,
|
||||||
state::{GamestreamServer, StateReadAccess, StateReader},
|
state::{GamestreamServer, StateReadAccess, StateReader},
|
||||||
@@ -81,12 +82,32 @@ impl crate::backend::Backend {
|
|||||||
self: ::std::sync::Arc<Self>,
|
self: ::std::sync::Arc<Self>,
|
||||||
body: salvo::oapi::extract::JsonBody<PostStreamStartParams>,
|
body: salvo::oapi::extract::JsonBody<PostStreamStartParams>,
|
||||||
req: &mut Request,
|
req: &mut Request,
|
||||||
|
depot: &mut Depot,
|
||||||
) -> AppResult<Json<PostStreamStartResponse>> {
|
) -> AppResult<Json<PostStreamStartResponse>> {
|
||||||
let standard_error = Err(crate::common::AppError {
|
let standard_error = Err(crate::common::AppError {
|
||||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
description: "Could not start stream".to_string(),
|
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 reader = self.state.read().await;
|
||||||
|
|
||||||
let server = match get_server(&reader, &body.server) {
|
let server = match get_server(&reader, &body.server) {
|
||||||
|
|||||||
Reference in New Issue
Block a user