ci: enforce clippy -D warnings and cargo audit as hard gates
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 12m18s
Build and Test / Build Server (Linux) (push) Successful in 14m11s
Build and Test / Security Audit (push) Successful in 5m32s
Build and Test / Build Summary (push) Successful in 9s

Flip both CI gates from informational to hard-fail (SPEC-001 quality gates):
- clippy: `-- -D warnings` on the server crate. Cleared the debt via clippy --fix
  (unused imports/style), targeted #[allow(dead_code)] on native-remote-control
  future API, and #[allow(clippy::too_many_arguments)] on 3 protocol-mirroring fns.
- cargo audit: hard-fail with documented per-ID --ignore flags (rsa RUSTSEC-2023-0071
  unfixable/unreachable in active tree; gtk-rs + glib Linux-only tray backend not
  compiled into the Windows agent; proc-macro-error build-time). New advisories fail.
- Move [profile.release] to the workspace root (it was silently ignored in the server
  member), activating lto/codegen-units/strip.

No behavioral changes. Reviewed and gates verified passing on the build host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 00:18:50 +00:00
parent 6e7e7c0ccb
commit ccc6ba9c02
21 changed files with 92 additions and 55 deletions

View File

@@ -57,11 +57,10 @@ jobs:
- name: Check formatting - name: Check formatting
run: cd server && cargo fmt --all -- --check run: cd server && cargo fmt --all -- --check
# Informational (warn-only) for now. The pre-spec codebase has ~65 lint warnings, # Hard gate: clippy must pass with zero warnings (-D warnings). Dead-code that is
# mostly dead-code for API the integration spec (native-remote-control) will wire. # future API surface for native-remote-control carries targeted #[allow(dead_code)].
# Re-tighten to `-- -D warnings` during the GC re-spec once that API is in use. - name: Run Clippy
- name: Run Clippy (informational) run: cd server && cargo clippy --all-targets --all-features -- -D warnings
run: cd server && cargo clippy --all-targets --all-features
- name: Build server - name: Build server
run: | run: |
@@ -143,12 +142,18 @@ jobs:
- name: Install cargo-audit - name: Install cargo-audit
run: cargo install cargo-audit run: cargo install cargo-audit
# Informational (warn-only) for now, like clippy. GuruConnect is a single Cargo workspace, # Hard gate: cargo audit must pass. GuruConnect is a single Cargo workspace, so one
# so one `cargo audit` at the root covers all members (agent + server) via the shared # `cargo audit` at the root covers all members (agent + server) via the shared Cargo.lock.
# Cargo.lock. The pre-spec dependency tree has known advisories; re-tighten to a hard gate # The advisories below are explicitly ignored with documented justifications; any NEW
# during the GC re-spec after a dependency refresh. # advisory fails the build.
- name: Run security audit (informational) # RUSTSEC-2023-0071 (rsa) ............. no fixed upgrade; optional/unreachable in active tree
run: cargo audit || echo "[WARNING] cargo audit reported advisories (informational; address in GC re-spec)" # RUSTSEC-2024-0413/-0416/-0412/-0418/
# -0415/-0420/-0419 (gtk-rs GTK3) ..... Linux-only tray-icon backend, not compiled into shipping Windows agent
# RUSTSEC-2024-0429 (glib) ............ Linux-only tray-icon backend, not compiled into shipping Windows agent
# RUSTSEC-2024-0370 (proc-macro-error) build-time proc-macro dependency, no runtime impact
- name: Run security audit
run: |
cargo audit --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2024-0413 --ignore RUSTSEC-2024-0416 --ignore RUSTSEC-2024-0412 --ignore RUSTSEC-2024-0418 --ignore RUSTSEC-2024-0415 --ignore RUSTSEC-2024-0420 --ignore RUSTSEC-2024-0419 --ignore RUSTSEC-2024-0429 --ignore RUSTSEC-2024-0370
build-summary: build-summary:
name: Build Summary name: Build Summary

View File

@@ -25,3 +25,8 @@ anyhow = "1"
thiserror = "1" thiserror = "1"
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
[profile.release]
lto = true
codegen-units = 1
strip = true

View File

@@ -60,8 +60,3 @@ prometheus-client = "0.22"
[build-dependencies] [build-dependencies]
prost-build = "0.13" prost-build = "0.13"
[profile.release]
lto = true
codegen-units = 1
strip = true

View File

@@ -1,13 +1,9 @@
//! Authentication API endpoints //! Authentication API endpoints
use axum::{ use axum::{extract::State, http::StatusCode, Json};
extract::{Request, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::auth::{verify_password, AuthenticatedUser, JwtConfig}; use crate::auth::{verify_password, AuthenticatedUser};
use crate::db; use crate::db;
use crate::AppState; use crate::AppState;

View File

@@ -1,7 +1,7 @@
//! Logout and token revocation endpoints //! Logout and token revocation endpoints
use axum::{ use axum::{
extract::{Path, Request, State}, extract::{Request, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
Json, Json,
}; };
@@ -97,7 +97,7 @@ pub struct RevokeUserRequest {
/// ///
/// For MVP, we're implementing the foundation but not the full user tracking. /// For MVP, we're implementing the foundation but not the full user tracking.
pub async fn revoke_user_tokens( pub async fn revoke_user_tokens(
State(state): State<AppState>, State(_state): State<AppState>,
admin: AuthenticatedUser, admin: AuthenticatedUser,
Json(req): Json<RevokeUserRequest>, Json(req): Json<RevokeUserRequest>,
) -> Result<Json<LogoutResponse>, (StatusCode, Json<ErrorResponse>)> { ) -> Result<Json<LogoutResponse>, (StatusCode, Json<ErrorResponse>)> {

View File

@@ -7,13 +7,13 @@
use axum::{ use axum::{
body::Body, body::Body,
extract::{Path, Query, State}, extract::Query,
http::{header, StatusCode}, http::{header, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{error, info, warn}; use tracing::{error, info};
/// Magic marker for embedded configuration (must match agent) /// Magic marker for embedded configuration (must match agent)
const MAGIC_MARKER: &[u8] = b"GURUCONFIG"; const MAGIC_MARKER: &[u8] = b"GURUCONFIG";

View File

@@ -8,7 +8,7 @@ pub mod releases;
pub mod users; pub mod users;
use axum::{ use axum::{
extract::{Path, Query, State}, extract::{Path, State},
Json, Json,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -78,12 +78,14 @@ impl From<crate::session::Session> for SessionInfo {
} }
/// List all active sessions /// List all active sessions
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn list_sessions(State(sessions): State<SessionManager>) -> Json<Vec<SessionInfo>> { pub async fn list_sessions(State(sessions): State<SessionManager>) -> Json<Vec<SessionInfo>> {
let sessions = sessions.list_sessions().await; let sessions = sessions.list_sessions().await;
Json(sessions.into_iter().map(SessionInfo::from).collect()) Json(sessions.into_iter().map(SessionInfo::from).collect())
} }
/// Get a specific session by ID /// Get a specific session by ID
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_session( pub async fn get_session(
State(sessions): State<SessionManager>, State(sessions): State<SessionManager>,
Path(id): Path<String>, Path(id): Path<String>,

View File

@@ -23,11 +23,14 @@ pub struct AuthenticatedUser {
pub user_id: String, pub user_id: String,
pub username: String, pub username: String,
pub role: String, pub role: String,
#[allow(dead_code)]
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub permissions: Vec<String>, pub permissions: Vec<String>,
} }
impl AuthenticatedUser { impl AuthenticatedUser {
/// Check if user has a specific permission /// Check if user has a specific permission
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub fn has_permission(&self, permission: &str) -> bool { pub fn has_permission(&self, permission: &str) -> bool {
if self.role == "admin" { if self.role == "admin" {
return true; return true;
@@ -54,6 +57,7 @@ impl From<Claims> for AuthenticatedUser {
/// Authenticated agent from API key /// Authenticated agent from API key
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct AuthenticatedAgent { pub struct AuthenticatedAgent {
pub agent_id: String, pub agent_id: String,
pub org_id: String, pub org_id: String,
@@ -61,11 +65,13 @@ pub struct AuthenticatedAgent {
/// JWT configuration stored in app state /// JWT configuration stored in app state
#[derive(Clone)] #[derive(Clone)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct AuthState { pub struct AuthState {
pub jwt_config: Arc<JwtConfig>, pub jwt_config: Arc<JwtConfig>,
} }
impl AuthState { impl AuthState {
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub fn new(jwt_secret: String, expiry_hours: i64) -> Self { pub fn new(jwt_secret: String, expiry_hours: i64) -> Self {
Self { Self {
jwt_config: Arc::new(JwtConfig::new(jwt_secret, expiry_hours)), jwt_config: Arc::new(JwtConfig::new(jwt_secret, expiry_hours)),
@@ -122,6 +128,7 @@ where
/// Optional authenticated user (doesn't reject if not authenticated) /// Optional authenticated user (doesn't reject if not authenticated)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct OptionalUser(pub Option<AuthenticatedUser>); pub struct OptionalUser(pub Option<AuthenticatedUser>);
#[axum::async_trait] #[axum::async_trait]
@@ -161,6 +168,7 @@ where
} }
/// Validate an agent API key (placeholder for MVP) /// Validate an agent API key (placeholder for MVP)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub fn validate_agent_key(_api_key: &str) -> Option<AuthenticatedAgent> { pub fn validate_agent_key(_api_key: &str) -> Option<AuthenticatedAgent> {
// TODO: Implement actual API key validation against database // TODO: Implement actual API key validation against database
// For now, accept any key for agent connections // For now, accept any key for agent connections

View File

@@ -5,6 +5,7 @@ use serde::Deserialize;
use std::env; use std::env;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct Config { pub struct Config {
/// Address to listen on (e.g., "0.0.0.0:8080") /// Address to listen on (e.g., "0.0.0.0:8080")
pub listen_addr: String, pub listen_addr: String,

View File

@@ -26,10 +26,13 @@ pub struct EventTypes;
impl EventTypes { impl EventTypes {
pub const SESSION_STARTED: &'static str = "session_started"; pub const SESSION_STARTED: &'static str = "session_started";
pub const SESSION_ENDED: &'static str = "session_ended"; pub const SESSION_ENDED: &'static str = "session_ended";
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub const SESSION_TIMEOUT: &'static str = "session_timeout"; pub const SESSION_TIMEOUT: &'static str = "session_timeout";
pub const VIEWER_JOINED: &'static str = "viewer_joined"; pub const VIEWER_JOINED: &'static str = "viewer_joined";
pub const VIEWER_LEFT: &'static str = "viewer_left"; pub const VIEWER_LEFT: &'static str = "viewer_left";
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub const STREAMING_STARTED: &'static str = "streaming_started"; pub const STREAMING_STARTED: &'static str = "streaming_started";
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub const STREAMING_STOPPED: &'static str = "streaming_stopped"; pub const STREAMING_STOPPED: &'static str = "streaming_stopped";
// Failed connection events (security audit trail) // Failed connection events (security audit trail)
@@ -75,6 +78,7 @@ pub async fn log_event(
} }
/// Get events for a session /// Get events for a session
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_session_events( pub async fn get_session_events(
pool: &PgPool, pool: &PgPool,
session_id: Uuid, session_id: Uuid,
@@ -88,6 +92,7 @@ pub async fn get_session_events(
} }
/// Get recent events (for dashboard) /// Get recent events (for dashboard)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_recent_events( pub async fn get_recent_events(
pool: &PgPool, pool: &PgPool,
limit: i64, limit: i64,
@@ -101,6 +106,7 @@ pub async fn get_recent_events(
} }
/// Get events by type /// Get events by type
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_events_by_type( pub async fn get_events_by_type(
pool: &PgPool, pool: &PgPool,
event_type: &str, event_type: &str,

View File

@@ -48,6 +48,7 @@ pub async fn upsert_machine(
} }
/// Update machine status and info /// Update machine status and info
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn update_machine_status( pub async fn update_machine_status(
pool: &PgPool, pool: &PgPool,
agent_id: &str, agent_id: &str,

View File

@@ -15,11 +15,7 @@ use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::info; use tracing::info;
pub use events::*;
pub use machines::*;
pub use releases::*; pub use releases::*;
pub use sessions::*;
pub use support_codes::*;
pub use users::*; pub use users::*;
/// Database connection pool wrapper /// Database connection pool wrapper

View File

@@ -20,6 +20,7 @@ pub struct Release {
} }
/// Create a new release /// Create a new release
#[allow(clippy::too_many_arguments)] // signature mirrors the relay/session protocol contract; refactor into a params struct tracked in docs/specs/native-remote-control/
pub async fn create_release( pub async fn create_release(
pool: &PgPool, pool: &PgPool,
version: &str, version: &str,
@@ -157,6 +158,7 @@ pub async fn update_machine_update_status(
} }
/// Get machines that need updates (version < latest stable) /// Get machines that need updates (version < latest stable)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_machines_needing_update( pub async fn get_machines_needing_update(
pool: &PgPool, pool: &PgPool,
latest_version: &str, latest_version: &str,

View File

@@ -64,6 +64,7 @@ pub async fn end_session(
} }
/// Get session by ID /// Get session by ID
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_session( pub async fn get_session(
pool: &PgPool, pool: &PgPool,
session_id: Uuid, session_id: Uuid,
@@ -75,6 +76,7 @@ pub async fn get_session(
} }
/// Get active sessions for a machine /// Get active sessions for a machine
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_active_sessions_for_machine( pub async fn get_active_sessions_for_machine(
pool: &PgPool, pool: &PgPool,
machine_id: Uuid, machine_id: Uuid,
@@ -88,6 +90,7 @@ pub async fn get_active_sessions_for_machine(
} }
/// Get recent sessions (for dashboard) /// Get recent sessions (for dashboard)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_recent_sessions(pool: &PgPool, limit: i64) -> Result<Vec<DbSession>, sqlx::Error> { pub async fn get_recent_sessions(pool: &PgPool, limit: i64) -> Result<Vec<DbSession>, sqlx::Error> {
sqlx::query_as::<_, DbSession>( sqlx::query_as::<_, DbSession>(
"SELECT * FROM connect_sessions ORDER BY started_at DESC LIMIT $1", "SELECT * FROM connect_sessions ORDER BY started_at DESC LIMIT $1",

View File

@@ -7,6 +7,7 @@ use uuid::Uuid;
/// Support code record from database /// Support code record from database
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct DbSupportCode { pub struct DbSupportCode {
pub id: Uuid, pub id: Uuid,
pub code: String, pub code: String,
@@ -21,6 +22,7 @@ pub struct DbSupportCode {
} }
/// Create a new support code /// Create a new support code
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn create_support_code( pub async fn create_support_code(
pool: &PgPool, pool: &PgPool,
code: &str, code: &str,
@@ -40,6 +42,7 @@ pub async fn create_support_code(
} }
/// Get support code by code string /// Get support code by code string
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_support_code( pub async fn get_support_code(
pool: &PgPool, pool: &PgPool,
code: &str, code: &str,
@@ -88,6 +91,7 @@ pub async fn mark_code_completed(pool: &PgPool, code: &str) -> Result<(), sqlx::
} }
/// Mark support code as cancelled /// Mark support code as cancelled
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn mark_code_cancelled(pool: &PgPool, code: &str) -> Result<(), sqlx::Error> { pub async fn mark_code_cancelled(pool: &PgPool, code: &str) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE connect_support_codes SET status = 'cancelled' WHERE code = $1") sqlx::query("UPDATE connect_support_codes SET status = 'cancelled' WHERE code = $1")
.bind(code) .bind(code)
@@ -97,6 +101,7 @@ pub async fn mark_code_cancelled(pool: &PgPool, code: &str) -> Result<(), sqlx::
} }
/// Get active support codes (pending or connected) /// Get active support codes (pending or connected)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_active_support_codes(pool: &PgPool) -> Result<Vec<DbSupportCode>, sqlx::Error> { pub async fn get_active_support_codes(pool: &PgPool) -> Result<Vec<DbSupportCode>, sqlx::Error> {
sqlx::query_as::<_, DbSupportCode>( sqlx::query_as::<_, DbSupportCode>(
"SELECT * FROM connect_support_codes WHERE status IN ('pending', 'connected') ORDER BY created_at DESC" "SELECT * FROM connect_support_codes WHERE status IN ('pending', 'connected') ORDER BY created_at DESC"
@@ -106,6 +111,7 @@ pub async fn get_active_support_codes(pool: &PgPool) -> Result<Vec<DbSupportCode
} }
/// Check if code exists and is valid for connection /// Check if code exists and is valid for connection
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn is_code_valid(pool: &PgPool, code: &str) -> Result<bool, sqlx::Error> { pub async fn is_code_valid(pool: &PgPool, code: &str) -> Result<bool, sqlx::Error> {
let result = sqlx::query_scalar::<_, bool>( let result = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM connect_support_codes WHERE code = $1 AND status = 'pending')", "SELECT EXISTS(SELECT 1 FROM connect_support_codes WHERE code = $1 AND status = 'pending')",
@@ -117,6 +123,7 @@ pub async fn is_code_valid(pool: &PgPool, code: &str) -> Result<bool, sqlx::Erro
} }
/// Check if code is cancelled /// Check if code is cancelled
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn is_code_cancelled(pool: &PgPool, code: &str) -> Result<bool, sqlx::Error> { pub async fn is_code_cancelled(pool: &PgPool, code: &str) -> Result<bool, sqlx::Error> {
let result = sqlx::query_scalar::<_, bool>( let result = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM connect_support_codes WHERE code = $1 AND status = 'cancelled')" "SELECT EXISTS(SELECT 1 FROM connect_support_codes WHERE code = $1 AND status = 'cancelled')"
@@ -128,6 +135,7 @@ pub async fn is_code_cancelled(pool: &PgPool, code: &str) -> Result<bool, sqlx::
} }
/// Link session to support code /// Link session to support code
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn link_session_to_code( pub async fn link_session_to_code(
pool: &PgPool, pool: &PgPool,
code: &str, code: &str,

View File

@@ -15,12 +15,15 @@ pub struct User {
pub role: String, pub role: String,
pub enabled: bool, pub enabled: bool,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
#[allow(dead_code)]
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>, pub last_login: Option<DateTime<Utc>>,
} }
/// User without password hash (for API responses) /// User without password hash (for API responses)
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub struct UserInfo { pub struct UserInfo {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
@@ -193,6 +196,7 @@ pub async fn set_user_permissions(
} }
/// Get user's accessible client IDs (empty = all access) /// Get user's accessible client IDs (empty = all access)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_user_client_access(pool: &PgPool, user_id: Uuid) -> Result<Vec<Uuid>> { pub async fn get_user_client_access(pool: &PgPool, user_id: Uuid) -> Result<Vec<Uuid>> {
let clients: Vec<(Uuid,)> = let clients: Vec<(Uuid,)> =
sqlx::query_as("SELECT client_id FROM user_client_access WHERE user_id = $1") sqlx::query_as("SELECT client_id FROM user_client_access WHERE user_id = $1")
@@ -226,6 +230,7 @@ pub async fn set_user_client_access(
} }
/// Check if user has access to a specific client /// Check if user has access to a specific client
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn user_has_client_access(pool: &PgPool, user_id: Uuid, client_id: Uuid) -> Result<bool> { pub async fn user_has_client_access(pool: &PgPool, user_id: Uuid, client_id: Uuid) -> Result<bool> {
// Admins have access to all // Admins have access to all
let user = get_user_by_id(pool, user_id).await?; let user = get_user_by_id(pool, user_id).await?;

View File

@@ -31,7 +31,7 @@ use axum::{
use serde::Deserialize; use serde::Deserialize;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::{info, Level}; use tracing::{info, Level};
@@ -76,7 +76,7 @@ async fn auth_layer(
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Initialize logging // Initialize logging
let _subscriber = FmtSubscriber::builder() FmtSubscriber::builder()
.with_max_level(Level::INFO) .with_max_level(Level::INFO)
.with_target(true) .with_target(true)
.init(); .init();
@@ -359,7 +359,7 @@ async fn main() -> Result<()> {
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
// SEC-11: Restricted CORS configuration // SEC-11: Restricted CORS configuration
.layer({ .layer({
let cors = CorsLayer::new() CorsLayer::new()
// Allow requests from the production domain and localhost (for development) // Allow requests from the production domain and localhost (for development)
.allow_origin([ .allow_origin([
"https://connect.azcomputerguru.com" "https://connect.azcomputerguru.com"
@@ -383,8 +383,7 @@ async fn main() -> Result<()> {
axum::http::header::ACCEPT, axum::http::header::ACCEPT,
]) ])
// Allow credentials (cookies, auth headers) // Allow credentials (cookies, auth headers)
.allow_credentials(true); .allow_credentials(true)
cors
}); });
// Start server // Start server
@@ -437,6 +436,7 @@ async fn list_codes(
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
struct ValidateParams { struct ValidateParams {
code: String, code: String,
} }

View File

@@ -288,6 +288,7 @@ pub async fn viewer_ws_handler(
} }
/// Handle an agent WebSocket connection /// Handle an agent WebSocket connection
#[allow(clippy::too_many_arguments)] // signature mirrors the relay/session protocol contract; refactor into a params struct tracked in docs/specs/native-remote-control/
async fn handle_agent_connection( async fn handle_agent_connection(
socket: WebSocket, socket: WebSocket,
sessions: SessionManager, sessions: SessionManager,
@@ -318,7 +319,7 @@ async fn handle_agent_connection(
}; };
let mut buf = Vec::new(); let mut buf = Vec::new();
if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() { if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
let _ = ws_sender.send(Message::Binary(buf.into())).await; let _ = ws_sender.send(Message::Binary(buf)).await;
} }
let _ = ws_sender.close().await; let _ = ws_sender.close().await;
return; return;
@@ -335,7 +336,7 @@ async fn handle_agent_connection(
info!("Session created: {} (agent in idle mode)", session_id); info!("Session created: {} (agent in idle mode)", session_id);
// Database: upsert machine and create session record // Database: upsert machine and create session record
let machine_id = if let Some(ref db) = db { let _machine_id = if let Some(ref db) = db {
match db::machines::upsert_machine(db.pool(), &agent_id, &agent_name, is_persistent).await { match db::machines::upsert_machine(db.pool(), &agent_id, &agent_name, is_persistent).await {
Ok(machine) => { Ok(machine) => {
// Create session record // Create session record
@@ -401,11 +402,7 @@ async fn handle_agent_connection(
let input_forward = tokio::spawn(async move { let input_forward = tokio::spawn(async move {
while let Some(input_data) = input_rx.recv().await { while let Some(input_data) = input_rx.recv().await {
let mut sender = ws_sender_input.lock().await; let mut sender = ws_sender_input.lock().await;
if sender if sender.send(Message::Binary(input_data)).await.is_err() {
.send(Message::Binary(input_data.into()))
.await
.is_err()
{
break; break;
} }
} }
@@ -435,7 +432,7 @@ async fn handle_agent_connection(
let mut buf = Vec::new(); let mut buf = Vec::new();
if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() { if prost::Message::encode(&disconnect_msg, &mut buf).is_ok() {
let mut sender = ws_sender_cancel.lock().await; let mut sender = ws_sender_cancel.lock().await;
let _ = sender.send(Message::Binary(buf.into())).await; let _ = sender.send(Message::Binary(buf)).await;
let _ = sender.close().await; let _ = sender.close().await;
} }
break; break;
@@ -651,11 +648,7 @@ async fn handle_viewer_connection(
// Task to forward frames from agent to this viewer // Task to forward frames from agent to this viewer
let frame_forward = tokio::spawn(async move { let frame_forward = tokio::spawn(async move {
while let Ok(frame_data) = frame_rx.recv().await { while let Ok(frame_data) = frame_rx.recv().await {
if ws_sender if ws_sender.send(Message::Binary(frame_data)).await.is_err() {
.send(Message::Binary(frame_data.into()))
.await
.is_err()
{
break; break;
} }
} }

View File

@@ -27,6 +27,7 @@ pub struct ViewerInfo {
} }
/// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval) /// Heartbeat timeout (90 seconds - 3x the agent's 30 second interval)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
const HEARTBEAT_TIMEOUT_SECS: u64 = 90; const HEARTBEAT_TIMEOUT_SECS: u64 = 90;
/// Session state /// Session state
@@ -68,6 +69,8 @@ struct SessionData {
frame_tx: FrameSender, frame_tx: FrameSender,
/// Channel for input events (viewer -> agent) /// Channel for input events (viewer -> agent)
input_tx: InputSender, input_tx: InputSender,
#[allow(dead_code)]
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
input_rx: Option<InputReceiver>, input_rx: Option<InputReceiver>,
/// Map of connected viewers (id -> info) /// Map of connected viewers (id -> info)
viewers: HashMap<ViewerId, ViewerInfo>, viewers: HashMap<ViewerId, ViewerInfo>,
@@ -176,6 +179,7 @@ impl SessionManager {
} }
/// Update agent status from heartbeat or status message /// Update agent status from heartbeat or status message
#[allow(clippy::too_many_arguments)] // signature mirrors the relay/session protocol contract; refactor into a params struct tracked in docs/specs/native-remote-control/
pub async fn update_agent_status( pub async fn update_agent_status(
&self, &self,
session_id: SessionId, session_id: SessionId,
@@ -225,6 +229,7 @@ impl SessionManager {
} }
/// Check if a session has timed out (no heartbeat for too long) /// Check if a session has timed out (no heartbeat for too long)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool { pub async fn is_session_timed_out(&self, session_id: SessionId) -> bool {
let sessions = self.sessions.read().await; let sessions = self.sessions.read().await;
if let Some(session_data) = sessions.get(&session_id) { if let Some(session_data) = sessions.get(&session_id) {
@@ -235,6 +240,7 @@ impl SessionManager {
} }
/// Get sessions that have timed out /// Get sessions that have timed out
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_timed_out_sessions(&self) -> Vec<SessionId> { pub async fn get_timed_out_sessions(&self) -> Vec<SessionId> {
let sessions = self.sessions.read().await; let sessions = self.sessions.read().await;
sessions sessions
@@ -471,11 +477,9 @@ impl SessionManager {
}; };
let mut buf = Vec::new(); let mut buf = Vec::new();
if admin_cmd.encode(&mut buf).is_ok() { if admin_cmd.encode(&mut buf).is_ok() && session_data.input_tx.send(buf).await.is_ok() {
if session_data.input_tx.send(buf).await.is_ok() { tracing::info!("Sent admin command {:?} to session {}", command, session_id);
tracing::info!("Sent admin command {:?} to session {}", command, session_id); return true;
return true;
}
} }
} }
false false

View File

@@ -36,6 +36,8 @@ pub enum CodeStatus {
/// Request to create a new support code /// Request to create a new support code
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateCodeRequest { pub struct CreateCodeRequest {
#[allow(dead_code)]
// TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub technician_id: Option<String>, pub technician_id: Option<String>,
pub technician_name: Option<String>, pub technician_name: Option<String>,
} }
@@ -172,6 +174,7 @@ impl SupportCodeManager {
} }
/// Get code by its code string /// Get code by its code string
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_code(&self, code: &str) -> Option<SupportCode> { pub async fn get_code(&self, code: &str) -> Option<SupportCode> {
let codes = self.codes.read().await; let codes = self.codes.read().await;
codes.get(code).cloned() codes.get(code).cloned()
@@ -209,6 +212,7 @@ impl SupportCodeManager {
} }
/// Check if a code is valid for connection (exists and is pending) /// Check if a code is valid for connection (exists and is pending)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn is_valid_for_connection(&self, code: &str) -> bool { pub async fn is_valid_for_connection(&self, code: &str) -> bool {
let codes = self.codes.read().await; let codes = self.codes.read().await;
codes codes
@@ -218,6 +222,7 @@ impl SupportCodeManager {
} }
/// List all codes (for dashboard) /// List all codes (for dashboard)
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn list_codes(&self) -> Vec<SupportCode> { pub async fn list_codes(&self) -> Vec<SupportCode> {
let codes = self.codes.read().await; let codes = self.codes.read().await;
codes.values().cloned().collect() codes.values().cloned().collect()
@@ -234,6 +239,7 @@ impl SupportCodeManager {
} }
/// Get code by session ID /// Get code by session ID
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub async fn get_by_session(&self, session_id: Uuid) -> Option<SupportCode> { pub async fn get_by_session(&self, session_id: Uuid) -> Option<SupportCode> {
let session_to_code = self.session_to_code.read().await; let session_to_code = self.session_to_code.read().await;
let code = session_to_code.get(&session_id)?; let code = session_to_code.get(&session_id)?;

View File

@@ -1,6 +1,5 @@
//! IP address extraction from WebSocket connections //! IP address extraction from WebSocket connections
use axum::extract::ConnectInfo;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
/// Extract IP address from Axum ConnectInfo /// Extract IP address from Axum ConnectInfo
@@ -12,11 +11,13 @@ use std::net::{IpAddr, SocketAddr};
/// // Use ip for logging /// // Use ip for logging
/// } /// }
/// ``` /// ```
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub fn extract_ip(addr: &SocketAddr) -> IpAddr { pub fn extract_ip(addr: &SocketAddr) -> IpAddr {
addr.ip() addr.ip()
} }
/// Extract IP address as string /// Extract IP address as string
#[allow(dead_code)] // TODO(native-remote-control): consumed by the integration API; see docs/specs/native-remote-control/
pub fn extract_ip_string(addr: &SocketAddr) -> String { pub fn extract_ip_string(addr: &SocketAddr) -> String {
addr.ip().to_string() addr.ip().to_string()
} }