Skip to main content

regent_sdk/hosts/handlers/
mod.rs

1//! Host connection handlers
2//!
3//! This module provides the connection handling infrastructure for Regent SDK.
4//! It includes handlers for different connection methods (SSH, localhost) and
5//! the traits that define the interface for host operations.
6
7pub mod localhost;
8pub mod ssh2;
9
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13use crate::error::RegentError;
14use crate::hosts::handlers::localhost::WhichUser;
15use crate::hosts::handlers::ssh2::Ssh2Auth;
16use crate::secrets::SecretProvider;
17use crate::secrets::SecretReference;
18use crate::{LocalHostHandler, Ssh2HostHandler};
19use crate::{command::CommandResult, hosts::privilege::Privilege};
20
21// Intermediary representation of a WhichUser
22// WhichUser holds secrets, TargetUser holds references to secrets
23
24/// Defines the type of user for a connection.
25///
26/// - `CurrentUser`: Use the currently authenticated user
27/// - `User`: Use a specific user identified by a secret reference
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum TargetUserKind {
30    /// Use the currently authenticated user for the connection.
31    CurrentUser,
32    /// Use a specific user, identified by a secret reference.
33    User(SecretReference),
34}
35
36/// Specifies the target user for a host connection.
37///
38/// This struct is used during the connection setup phase and holds references
39/// to secrets rather than the secrets themselves. The actual secrets are
40/// retrieved from the secret provider when the connection is established.
41///
42/// # Example
43///
44/// ```no_run
45/// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser};
46///
47/// // Connect as the current user
48/// let target = TargetUser::current_user();
49///
50/// // Connect as a specific user (secret will be retrieved from provider)
51/// let target = TargetUser::user("admin_credentials", Some("files".to_string()));
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55#[serde(rename_all = "PascalCase")]
56pub struct TargetUser {
57    /// The kind of user to connect as.
58    pub user_kind: TargetUserKind,
59}
60
61impl TargetUser {
62    /// Create a target user for the current user.
63    ///
64    /// # Example
65    ///
66    /// ```no_run
67    /// use regent_sdk::hosts::handlers::TargetUser;
68    ///
69    /// let target = TargetUser::current_user();
70    /// ```
71    pub fn current_user() -> Self {
72        Self {
73            user_kind: TargetUserKind::CurrentUser,
74        }
75    }
76
77    /// Create a target user for a specific user.
78    ///
79    /// # Arguments
80    ///
81    /// * `sec_ref` - Reference to the secret containing credentials
82    /// * `provider` - Optional name of the secret provider to use
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// use regent_sdk::hosts::handlers::TargetUser;
88    ///
89    /// let target = TargetUser::user("admin_password", Some("files".to_string()));
90    /// ```
91    pub fn user(sec_ref: &str, provider: Option<String>) -> Self {
92        Self {
93            user_kind: TargetUserKind::User(SecretReference::from(sec_ref, provider)),
94        }
95    }
96}
97
98/// Defines the method used to connect to a host.
99///
100/// # Variants
101///
102/// - `Localhost`: Connect to the local machine
103/// - `Ssh2`: Connect via SSH protocol
104///
105/// # Example
106///
107/// ```no_run
108/// use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser, Ssh2Auth};
109///
110/// // Localhost connection
111/// let method = ConnectionMethod::Localhost(TargetUser::current_user());
112///
113/// // SSH connection
114/// let method = ConnectionMethod::Ssh2(Ssh2Auth::username_password("creds", Some("files")));
115/// ```
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "PascalCase")]
118pub enum ConnectionMethod {
119    /// Connect to the local machine.
120    Localhost(TargetUser),
121    /// Connect via SSH protocol.
122    Ssh2(Ssh2Auth),
123}
124
125/// Trait defining the interface for host connection handlers.
126///
127/// All host handlers must implement this trait to provide the basic operations
128/// needed for managing connections and executing commands on remote hosts.
129///
130/// # Methods
131///
132/// - `connect`: Establish a connection to the host
133/// - `is_connected`: Check if currently connected
134/// - `disconnect`: Close the connection
135/// - `is_this_command_available`: Check if a command exists on the host
136/// - `run_command`: Execute a command on the host
137/// - `run_windows_command`: Execute a Windows command (available when the `windows` feature is enabled)
138/// - `get_file`: Retrieve a file from the host
139///
140/// # Example
141///
142/// Implementers of this trait can be used interchangeably through the [`Handler`] enum.
143pub trait HostHandler: Sized {
144    /// Establish a connection to the specified endpoint.
145    ///
146    /// # Arguments
147    ///
148    /// * `endpoint` - The host address to connect to (e.g., "192.168.1.100:22")
149    ///
150    /// # Returns
151    ///
152    /// `Ok(())` if connection was successful, or a [`RegentError`] if it failed.
153    async fn connect(
154        &mut self,
155        endpoint: &str,
156        // secret_provider: &Option<SecretProvider>,
157    ) -> Result<(), RegentError>;
158
159    /// Check if the handler is currently connected to a host.
160    ///
161    /// # Returns
162    ///
163    /// `true` if connected, `false` otherwise.
164    async fn is_connected(&mut self) -> bool;
165
166    /// Disconnect from the host.
167    ///
168    /// # Returns
169    ///
170    /// `Ok(())` if disconnection was successful, or a [`RegentError`] if it failed.
171    async fn disconnect(&mut self) -> Result<(), RegentError>;
172
173    /// Check if a specific command is available on the host.
174    ///
175    /// # Arguments
176    ///
177    /// * `command` - The command name to check
178    /// * `privilege` - The privilege level to check with
179    ///
180    /// # Returns
181    ///
182    /// `Ok(true)` if the command exists, `Ok(false)` if it doesn't,
183    /// or a [`RegentError`] if the check failed.
184    async fn is_this_command_available(
185        &mut self,
186        command: &str,
187        privilege: &Privilege,
188    ) -> Result<bool, RegentError>;
189
190    /// Execute a command on the host.
191    ///
192    /// # Arguments
193    ///
194    /// * `command` - The command to execute
195    /// * `privilege` - The privilege level to use
196    ///
197    /// # Returns
198    ///
199    /// A [`CommandResult`] containing the exit code, stdout, and stderr,
200    /// or a [`RegentError`] if execution failed.
201    async fn run_command(
202        &mut self,
203        command: &str,
204        privilege: &Privilege,
205    ) -> Result<CommandResult, RegentError>;
206
207    /// Execute a Windows command on the host.
208    ///
209    /// This method is specifically for Windows command execution.
210    ///
211    /// # Arguments
212    ///
213    /// * `command` - The Windows command to execute
214    ///
215    /// # Returns
216    ///
217    /// A [`CommandResult`] or a [`RegentError`] if execution failed.
218    #[cfg(feature = "windows")]
219    async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError>;
220
221    /// Retrieve the contents of a file from the host.
222    ///
223    /// # Arguments
224    ///
225    /// * `path` - The path to the file to retrieve
226    ///
227    /// # Returns
228    ///
229    /// The file contents as a byte vector, or a [`RegentError`] if retrieval failed.
230    async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError>;
231}
232
233/// Enum that can hold any type of host handler.
234///
235/// This enum provides a unified interface for working with different types
236/// of host handlers (local or SSH) through the [`HostHandler`] trait.
237///
238/// # Variants
239///
240/// - `LocalHost`: Handler for local machine connections
241/// - `Ssh2`: Handler for SSH connections
242///
243/// # Example
244///
245/// ```no_run
246/// use regent_sdk::{Handler, LocalHostHandler, Ssh2HostHandler, hosts::handlers::localhost::WhichUser};
247///
248/// // Create a local host handler
249/// let local = Handler::localhost(LocalHostHandler::from(WhichUser::CurrentUser));
250///
251/// // Create an SSH handler (requires connection details)
252/// // let ssh = Handler::ss2(Ssh2HostHandler::from(...).unwrap());
253/// ```
254// #[derive(Clone, Debug)]
255pub enum Handler {
256    /// Handler for local machine connections.
257    LocalHost(LocalHostHandler),
258    /// Handler for SSH connections.
259    Ssh2(Ssh2HostHandler),
260}
261
262impl Clone for Handler {
263    fn clone(&self) -> Self {
264        match self {
265            Handler::LocalHost(h) => Handler::LocalHost(h.clone()),
266            Handler::Ssh2(h) => Handler::Ssh2(h.clone()),
267        }
268    }
269}
270
271impl Handler {
272    /// Create a [`Handler`] from a [`LocalHostHandler`].
273    ///
274    /// # Arguments
275    ///
276    /// * `localhost_handler` - The local host handler to wrap
277    ///
278    /// # Example
279    ///
280    /// ```no_run
281    /// use regent_sdk::{Handler, LocalHostHandler, hosts::handlers::localhost::WhichUser};
282    ///
283    /// let handler = Handler::localhost(LocalHostHandler::from(WhichUser::CurrentUser));
284    /// ```
285    pub fn localhost(localhost_handler: LocalHostHandler) -> Self {
286        Handler::LocalHost(localhost_handler)
287    }
288
289    /// Create a [`Handler`] from a [`Ssh2HostHandler`].
290    ///
291    /// # Arguments
292    ///
293    /// * `ss2_handler` - The SSH host handler to wrap
294    ///
295    /// # Example
296    ///
297    /// ```no_run
298    /// use regent_sdk::{Handler, Ssh2HostHandler, hosts::handlers::ssh2::Ssh2AuthMethod};
299    ///
300    /// // Assuming we have valid auth details
301    /// // let ssh_handler = Ssh2HostHandler::from(Ssh2AuthMethod::Key(...)).unwrap();
302    /// // let handler = Handler::ss2(ssh_handler);
303    /// ```
304    pub fn ss2(ss2_handler: Ssh2HostHandler) -> Self {
305        Handler::Ssh2(ss2_handler)
306    }
307}
308
309impl HostHandler for Handler {
310    async fn connect(
311        &mut self,
312        endpoint: &str,
313        // secret_provider: &Option<SecretProvider>,
314    ) -> Result<(), RegentError> {
315        match self {
316            Handler::LocalHost(handler) => handler.connect(endpoint).await,
317            Handler::Ssh2(handler) => handler.connect(endpoint).await,
318        }
319    }
320
321    async fn is_connected(&mut self) -> bool {
322        match self {
323            Handler::LocalHost(handler) => handler.is_connected().await,
324            Handler::Ssh2(handler) => handler.is_connected().await,
325        }
326    }
327
328    async fn disconnect(&mut self) -> Result<(), RegentError> {
329        match self {
330            Handler::LocalHost(handler) => handler.disconnect().await,
331            Handler::Ssh2(handler) => handler.disconnect().await,
332        }
333    }
334
335    async fn is_this_command_available(
336        &mut self,
337        command: &str,
338        privilege: &Privilege,
339    ) -> Result<bool, RegentError> {
340        match self {
341            Handler::LocalHost(handler) => {
342                handler.is_this_command_available(command, privilege).await
343            }
344            Handler::Ssh2(handler) => handler.is_this_command_available(command, privilege).await,
345        }
346    }
347
348    async fn run_command(
349        &mut self,
350        command: &str,
351        privilege: &Privilege,
352    ) -> Result<CommandResult, RegentError> {
353        match self {
354            Handler::LocalHost(handler) => handler.run_command(command, privilege).await,
355            Handler::Ssh2(handler) => handler.run_command(command, privilege).await,
356        }
357    }
358
359    #[cfg(feature = "windows")]
360    async fn run_windows_command(&mut self, command: &str) -> Result<CommandResult, RegentError> {
361        match self {
362            Handler::LocalHost(handler) => handler.run_windows_command(command).await,
363            Handler::Ssh2(handler) => handler.run_windows_command(command).await,
364        }
365    }
366
367    async fn get_file(&mut self, path: PathBuf) -> Result<Vec<u8>, RegentError> {
368        match self {
369            Handler::LocalHost(handler) => handler.get_file(path).await,
370            Handler::Ssh2(handler) => handler.get_file(path).await,
371        }
372    }
373}
374
375// #[derive(Debug, Clone, Serialize, Deserialize)]
376// pub enum ConnectionDetails {
377//     // LocalHost(WhichUser),
378//     // Ssh2(NewSsh2ConnectionDetails),
379// }
380
381// TODO : add some syntax checks
382pub fn final_command(cmd: &str, privilege: &Privilege, user: &WhichUser) -> String {
383    match user {
384        WhichUser::CurrentUser => match privilege {
385            Privilege::None => format!("{} 2>&1", cmd),
386            Privilege::WithSudo => format!("sudo {} 2>&1", cmd),
387            Privilege::WithSudoRs => format!("sudo-rs {} 2>&1", cmd),
388        },
389        WhichUser::UsernamePassword(credentials) => match privilege {
390            Privilege::None => format!(
391                "echo {} | su - {} -c \"{}\" 2>&1", // echo <otherpwd> | su - otheruser -c "my command line"
392                credentials.password(),
393                credentials.username(),
394                cmd
395            ),
396            Privilege::WithSudo => format!(
397                "echo {} | sudo -S -u {} {} 2>&1",
398                credentials.password(),
399                credentials.username(),
400                cmd
401            ),
402            Privilege::WithSudoRs => format!(
403                "echo {} | sudo-rs -S -u {} {} 2>&1",
404                credentials.password(),
405                credentials.username(),
406                cmd
407            ),
408        },
409    }
410
411    // match privilege {
412    //     Privilege::None => {
413    //         let final_cmd = format!("{} 2>&1", cmd);
414    //         return final_cmd;
415    //     }
416    //     // Privilege::WithSuAsUser(credentials) => {
417    //     //     let final_cmd = format!("echo {} | su - {} -c {} 2>&1", credentials.password(), credentials.username(), cmd);
418    //     //     return final_cmd;
419    //     // }
420    //     Privilege::WithSudo => {
421    //         let final_cmd = format!("sudo {} 2>&1", cmd);
422    //         return final_cmd;
423    //     }
424    //     // Privilege::WithSudoAsUser(credentials) => {
425    //     //     let final_cmd = format!("echo {} | sudo -S -u {} {} 2>&1", credentials.password(), credentials.username(), cmd);
426    //     //     return final_cmd;
427    //     // }
428    //     Privilege::WithSudoRs => {
429    //         let final_cmd = format!("sudo-rs {} 2>&1", cmd);
430    //         return final_cmd;
431    //     }
432    //     // Privilege::WithSudoRsAsUser(credentials) => {
433    //     //     let final_cmd = format!("echo {} | sudo-rs -u {} {} 2>&1", credentials.password(), credentials.username(), cmd);
434    //     //     return final_cmd;
435    //     // }
436    // }
437}