Skip to content

Commit d798c4d

Browse files
authored
Merge pull request #128 from ecroteauwpi/feature/interactive-terminal-elevation
feat: add interactive Terminal elevation for managed macOS
2 parents b2af6f6 + 4175f00 commit d798c4d

13 files changed

Lines changed: 1935 additions & 79 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ xattr -cr /Applications/anylinuxfs-gui.app
5050

5151
Then you can open the app normally.
5252

53+
## Administrator authentication
54+
55+
The default **Native sudo** mode uses cached/native PAM authentication and falls back to the app's password dialog. This works for normal macOS administrator accounts.
56+
57+
For managed Macs where an endpoint privilege manager requires an interactive terminal, open **Preferences → Administrator authentication** and select **Interactive Terminal (managed Macs)**. Admin scans and mount operations then open an owner-only temporary `.command` file in Terminal. Complete the organization's approval or justification prompt there. LUKS and BitLocker secrets are requested directly by `anylinuxfs` in Terminal; they are not placed in the generated command file or its environment.
58+
59+
Interactive mount commands use macOS `script -q /dev/null` as a bidirectional pseudo-terminal relay and are not piped through an output-capture process. Terminal echo is disabled before the command and restored by a cleanup trap. This is required for endpoint privilege managers that place an elevated child on another pseudo-terminal, and prevents encryption secrets from being echoed or written to a handoff transcript. Output capture remains enabled only for non-secret discovery commands such as `list`.
60+
61+
Interactive Terminal mode never opens a window for automatic background refreshes. With Admin mode enabled, click **Refresh** when disks are connected or removed. A pending mount can be cancelled from the GUI; the app terminates its Terminal handoff and requests device-specific cleanup. This mode does not bypass organizational policy: the privilege manager can still approve or deny each command.
62+
63+
The Rust backend owns and persists the elevation setting. It cannot be changed while a privileged operation is active, so the authentication and cancellation behavior remain stable until that operation finishes.
64+
5365
## Screenshots
5466

5567
<picture>

src-tauri/src/cli.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ use std::fs;
44
use std::path::{Path, PathBuf};
55
use std::time::{Duration, Instant};
66
use std::sync::OnceLock;
7+
use crate::elevation::{
8+
execute_in_terminal, ElevationMode, ElevationState, TerminalExecutionError,
9+
TerminalInteraction,
10+
};
11+
12+
#[derive(Debug, Clone)]
13+
pub enum CommandExecutionError {
14+
InteractionRequired,
15+
Cancelled,
16+
TimedOut,
17+
Failed(String),
18+
}
19+
20+
impl CommandExecutionError {
21+
pub fn message(&self) -> String {
22+
match self {
23+
Self::InteractionRequired => "ALFS_SILENT_AUTH_EXPIRED".to_string(),
24+
Self::Cancelled => "Interactive Terminal operation was cancelled".to_string(),
25+
Self::TimedOut => "Interactive Terminal operation timed out".to_string(),
26+
Self::Failed(message) => message.clone(),
27+
}
28+
}
29+
}
730

831
/// Sanitize error output to avoid exposing sensitive system details
932
/// Logs the full error for debugging but returns a user-friendly message
@@ -22,6 +45,9 @@ fn sanitize_error(stdout: &str, stderr: &str) -> String {
2245
if combined.contains("Permission denied") {
2346
return "Permission denied - try running with administrator privileges".to_string();
2447
}
48+
if combined.contains("Execution blocked") || combined.contains("does not have Admin rights") {
49+
return "Administrator elevation was blocked by system policy".to_string();
50+
}
2551
if combined.contains("Device busy") || combined.contains("resource busy") {
2652
return "Device is busy - close any applications using it and try again".to_string();
2753
}
@@ -172,6 +198,54 @@ pub fn execute_command(args: &[&str], needs_sudo: bool, passphrase: Option<&str>
172198
}
173199
}
174200

201+
/// Execute an anylinuxfs command using the Rust-owned elevation policy.
202+
pub fn execute_command_with_elevation(
203+
args: &[&str],
204+
needs_sudo: bool,
205+
passphrase: Option<&str>,
206+
silent: bool,
207+
elevation_mode: ElevationMode,
208+
elevation_state: &ElevationState,
209+
terminal_interaction: TerminalInteraction,
210+
) -> Result<String, CommandExecutionError> {
211+
if needs_sudo {
212+
match elevation_mode {
213+
ElevationMode::Native => execute_with_sudo(args, passphrase, silent)
214+
.map_err(|error| {
215+
if error == "ALFS_SILENT_AUTH_EXPIRED" {
216+
CommandExecutionError::InteractionRequired
217+
} else {
218+
CommandExecutionError::Failed(error)
219+
}
220+
}),
221+
ElevationMode::InteractiveTerminal => execute_in_terminal(
222+
elevation_state,
223+
get_anylinuxfs_path().ok_or_else(|| {
224+
CommandExecutionError::Failed(
225+
"anylinuxfs CLI not found in PATH or standard locations".to_string(),
226+
)
227+
})?,
228+
args,
229+
silent,
230+
terminal_interaction,
231+
)
232+
.map_err(|error| match error {
233+
TerminalExecutionError::InteractionRequired => {
234+
CommandExecutionError::InteractionRequired
235+
}
236+
TerminalExecutionError::Cancelled => CommandExecutionError::Cancelled,
237+
TerminalExecutionError::TimedOut => CommandExecutionError::TimedOut,
238+
TerminalExecutionError::CommandFailed { output, .. } if !output.is_empty() => {
239+
CommandExecutionError::Failed(sanitize_error(&output, ""))
240+
}
241+
other => CommandExecutionError::Failed(other.to_string()),
242+
}),
243+
}
244+
} else {
245+
execute_direct(args, passphrase).map_err(CommandExecutionError::Failed)
246+
}
247+
}
248+
175249
fn execute_direct(args: &[&str], passphrase: Option<&str>) -> Result<String, String> {
176250
let cli_path = get_anylinuxfs_path()
177251
.ok_or_else(|| "anylinuxfs CLI not found in PATH or standard locations".to_string())?;
@@ -391,3 +465,17 @@ osascript -e 'Tell application "System Events" to display dialog "anylinuxfs req
391465

392466
Ok(path.to_string_lossy().to_string())
393467
}
468+
469+
#[cfg(test)]
470+
mod tests {
471+
use super::*;
472+
473+
#[test]
474+
fn endpoint_privilege_denial_has_a_specific_error() {
475+
let message = sanitize_error(
476+
"Execution blocked: user does not have Admin rights",
477+
"",
478+
);
479+
assert_eq!(message, "Administrator elevation was blocked by system policy");
480+
}
481+
}

0 commit comments

Comments
 (0)