From 61098ad4073567a42bef0470e835d87be4f3e512 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Fri, 11 Jul 2025 15:55:51 -0400 Subject: [PATCH 1/8] add set scaffolding --- sshdconfig/src/args.rs | 2 +- sshdconfig/src/get.rs | 4 +- sshdconfig/src/set.rs | 91 +++++++++++++++++++++++-- sshdconfig/src/util.rs | 4 +- sshdconfig/tests/defaultshell.tests.ps1 | 19 +----- 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/sshdconfig/src/args.rs b/sshdconfig/src/args.rs index af47398e6..cd6b1c7f4 100644 --- a/sshdconfig/src/args.rs +++ b/sshdconfig/src/args.rs @@ -39,7 +39,7 @@ pub enum Command { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] pub struct DefaultShell { - pub shell: Option, + pub shell: String, pub cmd_option: Option, pub escape_arguments: Option, } diff --git a/sshdconfig/src/get.rs b/sshdconfig/src/get.rs index 6783f0d54..1c3deaeef 100644 --- a/sshdconfig/src/get.rs +++ b/sshdconfig/src/get.rs @@ -34,12 +34,12 @@ pub fn invoke_get(exclude_defaults: bool, input: Option<&String>, setting: &Sett fn get_default_shell() -> Result<(), SshdConfigError> { let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL.to_string()), None)?; let default_shell: Registry = registry_helper.get()?; - let mut shell = None; + let mut shell = String::new(); // default_shell is a single string consisting of the shell exe path if let Some(value) = default_shell.value_data { match value { RegistryValueData::String(s) => { - shell = Some(s); + shell = s; } _ => return Err(SshdConfigError::InvalidInput(t!("get.defaultShellMustBeString").to_string())), } diff --git a/sshdconfig/src/set.rs b/sshdconfig/src/set.rs index 349e36cf3..9a9599fe1 100644 --- a/sshdconfig/src/set.rs +++ b/sshdconfig/src/set.rs @@ -8,9 +8,13 @@ use { crate::metadata::windows::{DEFAULT_SHELL, DEFAULT_SHELL_CMD_OPTION, DEFAULT_SHELL_ESCAPE_ARGS, REGISTRY_PATH}, }; +use rust_i18n::t; +use serde_json::{Map, Value}; +use tracing::debug; + use crate::args::DefaultShell; use crate::error::SshdConfigError; -use rust_i18n::t; +use crate::util::{invoke_sshd_config_validation, SshdCmdArgs}; /// Invoke the set command. /// @@ -20,17 +24,22 @@ use rust_i18n::t; pub fn invoke_set(input: &str) -> Result<(), SshdConfigError> { match serde_json::from_str::(input) { Ok(default_shell) => { + debug!("default_shell: {:?}", default_shell); set_default_shell(default_shell.shell, default_shell.cmd_option, default_shell.escape_arguments) }, - Err(e) => { - Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string())) + Err(_) => { + match serde_json::from_str::>(input) { + Ok(sshd_config) => set_sshd_config(sshd_config), + Err(e) => Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string())), + } } } } #[cfg(windows)] -fn set_default_shell(shell: Option, cmd_option: Option, escape_arguments: Option) -> Result<(), SshdConfigError> { - if let Some(shell) = shell { +fn set_default_shell(shell: String, cmd_option: Option, escape_arguments: Option) -> Result<(), SshdConfigError> { + debug!("Setting default shell"); + if !shell.is_empty() { // TODO: if shell contains quotes, we need to remove them let shell_path = Path::new(&shell); if shell_path.is_relative() && shell_path.components().any(|c| c == std::path::Component::ParentDir) { @@ -83,3 +92,75 @@ fn remove_registry(name: &str) -> Result<(), SshdConfigError> { registry_helper.remove()?; Ok(()) } + +fn set_sshd_config(input: Map) -> Result<(), SshdConfigError> { + // this should be its own helper function that checks that the value makes sense for the key + debug!("Writing temporary sshd_config file"); + let mut config_text = String::new(); + for (key, value) in &input { + if let Some(value_str) = value.as_str() { + config_text.push_str(&format!("{} {}\n", key, value_str)); + } else { + return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string())); + } + } + + // this should also be a helper function potentially + let temp_file = tempfile::Builder::new() + .prefix("sshd_config_temp_") + .suffix(".tmp") + .tempfile()?; + let temp_path = temp_file.path().to_string_lossy().into_owned(); + let (file, path) = temp_file.keep()?; + debug!("temporary file created at: {}", temp_path); + std::fs::write(&temp_path, &config_text) + .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; + drop(file); + + let args = Some( + SshdCmdArgs { + filepath: Some(temp_path.clone()), + additional_args: None, + } + ); + + debug!("Validating temporary sshd_config file"); + invoke_sshd_config_validation(args)?; + + // sshd_config path should be defined based on the system, typically at /etc/ssh/sshd_config or C:\ProgramData\ssh\sshd_config + let sshd_config_path = if cfg!(windows) { + "C:\\ProgramData\\ssh\\sshd_config" + } else { + "/etc/ssh/sshd_config" + }; + let sshd_config_path = Path::new(sshd_config_path); + + if sshd_config_path.exists() { + let mut sshd_config_content = String::new(); + if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(sshd_config_path) { + use std::io::Read; + file.read_to_string(&mut sshd_config_content) + .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; + } else { + return Err(SshdConfigError::CommandError(t!("set.sshdConfigReadFailed", path = sshd_config_path.display()).to_string())); + } + // Check if the first line contains "managed by dsc sshdconfig resource" + if !sshd_config_content.starts_with("# managed by dsc sshdconfig resource") { + // If not, create a backup of the existing file + debug!("Backing up existing sshd_config file"); + let backup_path = format!("{}.bak", sshd_config_path.display()); + std::fs::write(&backup_path, &sshd_config_content) + .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; + debug!("Backup created at: {}", backup_path); + } + } + + std::fs::write(sshd_config_path, &config_text) + .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; + + if let Err(e) = std::fs::remove_file(&path) { + debug!("Failed to clean up temporary file {}: {}", path.display(), e); + } + + Ok(()) +} diff --git a/sshdconfig/src/util.rs b/sshdconfig/src/util.rs index c842a2b44..4eb629825 100644 --- a/sshdconfig/src/util.rs +++ b/sshdconfig/src/util.rs @@ -15,9 +15,9 @@ use crate::parser::parse_text_to_map; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct SshdCmdArgs { #[serde(skip_serializing_if = "Option::is_none")] - filepath: Option, + pub filepath: Option, #[serde(rename = "additionalArgs", skip_serializing_if = "Option::is_none")] - additional_args: Option>, + pub additional_args: Option>, } /// Enable tracing. diff --git a/sshdconfig/tests/defaultshell.tests.ps1 b/sshdconfig/tests/defaultshell.tests.ps1 index b93f9a54c..c8c113c4b 100644 --- a/sshdconfig/tests/defaultshell.tests.ps1 +++ b/sshdconfig/tests/defaultshell.tests.ps1 @@ -140,10 +140,10 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows) { $LASTEXITCODE | Should -Not -Be 0 } - It 'Should clear default shell when set to null' { + It 'Should clear default shell when set to empty string' { Set-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value "C:\Windows\System32\cmd.exe" - $inputConfig = @{ shell = $null } | ConvertTo-Json + $inputConfig = @{ shell = "" } | ConvertTo-Json sshdconfig set --input $inputConfig $LASTEXITCODE | Should -Be 0 @@ -175,21 +175,6 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows) { $retrievedConfig.escape_arguments | Should -Be $originalConfig.escape_arguments } } - - Context 'Set default shell with null value' { - It 'Should clear existing default shell when set to null' { - $testShell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" - New-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value $testShell - - $inputConfig = @{ shell = $null } | ConvertTo-Json - - sshdconfig set --input $inputConfig - $LASTEXITCODE | Should -Be 0 - - $result = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue - $result | Should -BeNullOrEmpty - } - } } Describe 'Default Shell Configuration Error Handling on Non-Windows Platforms' -Skip:($IsWindows) { From 2797965ef0ecb72a46d522608f4d54fe4307dbb7 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 19 Nov 2025 15:52:24 -0500 Subject: [PATCH 2/8] add initial set functionality for normal keywords and clobber=TRUE only --- resources/sshdconfig/locales/en-us.toml | 19 +- resources/sshdconfig/src/args.rs | 4 +- resources/sshdconfig/src/inputs.rs | 8 +- resources/sshdconfig/src/main.rs | 4 +- resources/sshdconfig/src/metadata.rs | 6 + resources/sshdconfig/src/set.rs | 116 ++++++----- resources/sshdconfig/src/util.rs | 83 +++++--- .../sshdconfig/sshd-windows.dsc.resource.json | 2 + .../sshdconfig/tests/defaultshell.tests.ps1 | 14 +- .../sshdconfig/tests/sshdconfig.set.tests.ps1 | 185 ++++++++++++++++++ 10 files changed, 350 insertions(+), 91 deletions(-) create mode 100644 resources/sshdconfig/tests/sshdconfig.set.tests.ps1 diff --git a/resources/sshdconfig/locales/en-us.toml b/resources/sshdconfig/locales/en-us.toml index 91215af71..996feb935 100644 --- a/resources/sshdconfig/locales/en-us.toml +++ b/resources/sshdconfig/locales/en-us.toml @@ -49,14 +49,27 @@ unknownNode = "unknown node: '%{kind}'" unknownNodeType = "unknown node type: '%{node}'" [set] -failedToParseInput = "failed to parse input as DefaultShell with error: '%{error}'" +backingUpConfig = "Backing up existing sshd_config file" +backupCreated = "Backup created at: %{path}" +cleanupFailed = "Failed to clean up temporary file %{path}: %{error}" +clobberFalseUnsupported = "clobber=false is not yet supported for sshd_config resource" +configDoesNotExist = "sshd_config file does not exist, no backup created" +defaultShellDebug = "default_shell: %{shell}" +failedToParseConfig = "failed to parse input for sshd config with error: '%{error}'" +failedToParseDefaultShell = "failed to parse input for DefaultShell with error: '%{error}'" +settingDefaultShell = "Setting default shell" shellPathDoesNotExist = "shell path does not exist: '%{shell}'" shellPathMustNotBeRelative = "shell path must not be relative" +tempFileCreated = "temporary file created at: %{path}" +validatingTempConfig = "Validating temporary sshd_config file" +writingTempConfig = "Writing temporary sshd_config file" [util] -includeDefaultsMustBeBoolean = "_includeDefaults must be true or false" -inputMustBeEmpty = "get command does not support filtering based on input settings" +cleanupFailed = "Failed to clean up temporary file %{path}: %{error}" +inputMustBeBoolean = "value of '%{input}' must be true or false" +inputWillBeIgnored = "get command does not support filtering, ignoring input" sshdConfigNotFound = "sshd_config not found at path: '%{path}'" sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'" sshdElevation = "elevated security context required" +tempFileCreated = "temporary file created at: %{path}" tracingInitError = "Failed to initialize tracing" diff --git a/resources/sshdconfig/src/args.rs b/resources/sshdconfig/src/args.rs index 616ec4073..d1d012141 100644 --- a/resources/sshdconfig/src/args.rs +++ b/resources/sshdconfig/src/args.rs @@ -24,7 +24,9 @@ pub enum Command { /// Set default shell, eventually to be used for `sshd_config` and repeatable keywords Set { #[clap(short = 'i', long, help = t!("args.setInput").to_string())] - input: String + input: String, + #[clap(short = 's', long, hide = true)] + setting: Setting, }, /// Export `sshd_config`, eventually to be used for repeatable keywords Export { diff --git a/resources/sshdconfig/src/inputs.rs b/resources/sshdconfig/src/inputs.rs index 97507d196..01fd9f1b8 100644 --- a/resources/sshdconfig/src/inputs.rs +++ b/resources/sshdconfig/src/inputs.rs @@ -3,9 +3,12 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use std::path::PathBuf; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct CommandInfo { + #[serde(rename = "_clobber")] + pub clobber: bool, /// Switch to include defaults in the output #[serde(rename = "_includeDefaults")] pub include_defaults: bool, @@ -21,6 +24,7 @@ impl CommandInfo { /// Create a new `CommandInfo` instance. pub fn new(include_defaults: bool) -> Self { Self { + clobber: false, include_defaults, input: Map::new(), metadata: Metadata::new(), @@ -33,7 +37,7 @@ impl CommandInfo { pub struct Metadata { /// Filepath for the `sshd_config` file to be processed #[serde(skip_serializing_if = "Option::is_none")] - pub filepath: Option + pub filepath: Option } impl Metadata { @@ -49,7 +53,7 @@ impl Metadata { pub struct SshdCommandArgs { /// the path to the `sshd_config` file to be processed #[serde(skip_serializing_if = "Option::is_none")] - pub filepath: Option, + pub filepath: Option, /// additional arguments to pass to the sshd -T command #[serde(rename = "additionalArgs", skip_serializing_if = "Option::is_none")] pub additional_args: Option>, diff --git a/resources/sshdconfig/src/main.rs b/resources/sshdconfig/src/main.rs index bf6440ee5..f473a47d3 100644 --- a/resources/sshdconfig/src/main.rs +++ b/resources/sshdconfig/src/main.rs @@ -57,9 +57,9 @@ fn main() { println!("{}", serde_json::to_string(&schema).unwrap()); Ok(Map::new()) }, - Command::Set { input } => { + Command::Set { input, setting } => { debug!("{}", t!("main.set", input = input).to_string()); - invoke_set(input) + invoke_set(input, setting) }, }; diff --git a/resources/sshdconfig/src/metadata.rs b/resources/sshdconfig/src/metadata.rs index 7a94c5109..4b8ec0956 100644 --- a/resources/sshdconfig/src/metadata.rs +++ b/resources/sshdconfig/src/metadata.rs @@ -39,6 +39,12 @@ pub const REPEATABLE_KEYWORDS: [&str; 12] = [ "subsystem" ]; + +pub const SSHD_CONFIG_HEADER: &str = "# This file is managed by Microsoft DSC sshdconfig resource."; +pub const SSHD_CONFIG_DEFAULT_PATH_UNIX: &str = "/etc/ssh/sshd_config"; +// For Windows, full path is constructed at runtime using ProgramData environment variable +pub const SSHD_CONFIG_DEFAULT_PATH_WINDOWS: &str = "\\ssh\\sshd_config"; + #[cfg(windows)] pub mod windows { pub const REGISTRY_PATH: &str = "HKLM\\SOFTWARE\\OpenSSH"; diff --git a/resources/sshdconfig/src/set.rs b/resources/sshdconfig/src/set.rs index c092c22a4..2948076ac 100644 --- a/resources/sshdconfig/src/set.rs +++ b/resources/sshdconfig/src/set.rs @@ -12,26 +12,43 @@ use rust_i18n::t; use serde_json::{Map, Value}; use tracing::debug; -use crate::args::DefaultShell; +use crate::args::{DefaultShell, Setting}; use crate::error::SshdConfigError; -use crate::util::{invoke_sshd_config_validation, SshdCmdArgs}; +use crate::inputs::{CommandInfo, SshdCommandArgs}; +use crate::metadata::SSHD_CONFIG_HEADER; +use crate::util::{build_command_info, get_default_sshd_config_path, invoke_sshd_config_validation}; /// Invoke the set command. /// /// # Errors /// /// This function will return an error if the desired settings cannot be applied. -pub fn invoke_set(input: &str) -> Result, SshdConfigError> { - match serde_json::from_str::(input) { - Ok(default_shell) => { - debug!("default_shell: {:?}", default_shell); - set_default_shell(default_shell.shell, default_shell.cmd_option, default_shell.escape_arguments)?; - Ok(Map::new()) +pub fn invoke_set(input: &str, setting: &Setting) -> Result, SshdConfigError> { + match setting { + Setting::SshdConfig => { + debug!("{} {:?}", t!("set.settingSshdConfig").to_string(), setting); + let cmd_info = build_command_info(Some(&input.to_string()), false)?; + match set_sshd_config(&cmd_info) { + Ok(_) => Ok(Map::new()), + Err(e) => Err(e), + } }, - Err(_) => { - match serde_json::from_str::>(input) { - Ok(sshd_config) => set_sshd_config(sshd_config), - Err(e) => Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string())), + Setting::WindowsGlobal => { + debug!("{} {:?}", t!("set.settingWindowsGlobal").to_string(), setting); + match serde_json::from_str::(input) { + Ok(default_shell) => { + debug!("{}", t!("set.defaultShellDebug", shell = format!("{:?}", default_shell))); + // if default_shell.shell is Some, we should pass that into set default shell + // otherwise pass in an empty string + let shell = if let Some(shell) = default_shell.shell.clone() { + shell + } else { + String::new() + }; + set_default_shell(shell, default_shell.cmd_option, default_shell.escape_arguments)?; + Ok(Map::new()) + }, + Err(e) => Err(SshdConfigError::InvalidInput(t!("set.failedToParseDefaultShell", error = e).to_string())), } } } @@ -39,7 +56,7 @@ pub fn invoke_set(input: &str) -> Result, SshdConfigError> { #[cfg(windows)] fn set_default_shell(shell: String, cmd_option: Option, escape_arguments: Option) -> Result<(), SshdConfigError> { - debug!("Setting default shell"); + debug!("{}", t!("set.settingDefaultShell")); if !shell.is_empty() { // TODO: if shell contains quotes, we need to remove them let shell_path = Path::new(&shell); @@ -94,74 +111,79 @@ fn remove_registry(name: &str) -> Result<(), SshdConfigError> { Ok(()) } -fn set_sshd_config(input: Map) -> Result<(), SshdConfigError> { - // this should be its own helper function that checks that the value makes sense for the key - debug!("Writing temporary sshd_config file"); - let mut config_text = String::new(); - for (key, value) in &input { - if let Some(value_str) = value.as_str() { - config_text.push_str(&format!("{} {}\n", key, value_str)); - } else { - return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string())); +fn set_sshd_config(cmd_info: &CommandInfo) -> Result<(), SshdConfigError> { + // this should be its own helper function that checks that the value makes sense for the key type + // i.e. if the key can be repeated or have multiple values, etc. + // or if the value is something besides a string (like an object to convert back into a comma-separated list) + debug!("{}", t!("set.writingTempConfig")); + let mut config_text = SSHD_CONFIG_HEADER.to_string() + "\n"; + if cmd_info.clobber { + for (key, value) in &cmd_info.input { + if let Some(value_str) = value.as_str() { + config_text.push_str(&format!("{} {}\n", key, value_str)); + } else { + return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string())); + } } } + else { + /* TODO: preserve existing settings that are not in input, probably need to call get */ + return Err(SshdConfigError::InvalidInput(t!("set.clobberFalseUnsupported").to_string())); + } - // this should also be a helper function potentially + // Write input to a temporary file and validate it with SSHD -T let temp_file = tempfile::Builder::new() .prefix("sshd_config_temp_") .suffix(".tmp") .tempfile()?; - let temp_path = temp_file.path().to_string_lossy().into_owned(); + let temp_path = temp_file.path().to_path_buf(); let (file, path) = temp_file.keep()?; - debug!("temporary file created at: {}", temp_path); + debug!("{}", t!("set.tempFileCreated", path = temp_path.display())); std::fs::write(&temp_path, &config_text) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; drop(file); let args = Some( - SshdCmdArgs { - filepath: Some(temp_path.clone()), + SshdCommandArgs { + filepath: Some(temp_path), additional_args: None, } ); - debug!("Validating temporary sshd_config file"); - invoke_sshd_config_validation(args)?; + debug!("{}", t!("set.validatingTempConfig")); + let result = invoke_sshd_config_validation(args); + // Always cleanup temp file, regardless of result success or failure + if let Err(e) = std::fs::remove_file(&path) { + debug!("{}", t!("set.cleanupFailed", path = path.display(), error = e)); + } + // Propagate failure, if any + result?; - // sshd_config path should be defined based on the system, typically at /etc/ssh/sshd_config or C:\ProgramData\ssh\sshd_config - let sshd_config_path = if cfg!(windows) { - "C:\\ProgramData\\ssh\\sshd_config" - } else { - "/etc/ssh/sshd_config" - }; - let sshd_config_path = Path::new(sshd_config_path); + let sshd_config_path = get_default_sshd_config_path(cmd_info.metadata.filepath.clone()); if sshd_config_path.exists() { let mut sshd_config_content = String::new(); - if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(sshd_config_path) { + if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(&sshd_config_path) { use std::io::Read; file.read_to_string(&mut sshd_config_content) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; } else { return Err(SshdConfigError::CommandError(t!("set.sshdConfigReadFailed", path = sshd_config_path.display()).to_string())); } - // Check if the first line contains "managed by dsc sshdconfig resource" - if !sshd_config_content.starts_with("# managed by dsc sshdconfig resource") { - // If not, create a backup of the existing file - debug!("Backing up existing sshd_config file"); + if !sshd_config_content.starts_with(SSHD_CONFIG_HEADER) { + // If config file is not already managed by this resource, create a backup of the existing file + debug!("{}", t!("set.backingUpConfig")); let backup_path = format!("{}.bak", sshd_config_path.display()); std::fs::write(&backup_path, &sshd_config_content) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; - debug!("Backup created at: {}", backup_path); + debug!("{}", t!("set.backupCreated", path = backup_path)); } + } else { + debug!("{}", t!("set.configDoesNotExist")); } - std::fs::write(sshd_config_path, &config_text) + std::fs::write(&sshd_config_path, &config_text) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; - if let Err(e) = std::fs::remove_file(&path) { - debug!("Failed to clean up temporary file {}: {}", path.display(), e); - } - Ok(()) } diff --git a/resources/sshdconfig/src/util.rs b/resources/sshdconfig/src/util.rs index d095f3bb8..425973438 100644 --- a/resources/sshdconfig/src/util.rs +++ b/resources/sshdconfig/src/util.rs @@ -3,12 +3,13 @@ use rust_i18n::t; use serde_json::{Map, Value}; -use std::{path::Path, process::Command}; +use std::{path::PathBuf, process::Command}; use tracing::debug; use tracing_subscriber::{EnvFilter, filter::LevelFilter, Layer, prelude::__tracing_subscriber_SubscriberExt}; use crate::error::SshdConfigError; use crate::inputs::{CommandInfo, Metadata, SshdCommandArgs}; +use crate::metadata::{SSHD_CONFIG_DEFAULT_PATH_UNIX, SSHD_CONFIG_DEFAULT_PATH_WINDOWS}; use crate::parser::parse_text_to_map; /// Enable tracing. @@ -34,6 +35,21 @@ pub fn enable_tracing() { } } +/// Get the sshd_config path based on the provided input +/// If not provided, get default path for the current platform. +/// On Windows, uses the ProgramData environment variable. +/// On Unix-like systems, uses the standard path. +pub fn get_default_sshd_config_path(input: Option) -> PathBuf { + if let Some(path) = input { + path + } else if cfg!(windows) { + let program_data = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into()); + PathBuf::from(format!("{}{}", program_data, SSHD_CONFIG_DEFAULT_PATH_WINDOWS)) + } else { + PathBuf::from(SSHD_CONFIG_DEFAULT_PATH_UNIX) + } +} + /// Invoke sshd -T. /// /// # Errors @@ -45,7 +61,7 @@ pub fn invoke_sshd_config_validation(args: Option) -> Result Result, SshdConfigError> { .tempfile()?; // on Windows, sshd cannot read from the file if it is still open - let temp_path = temp_file.path().to_string_lossy().into_owned(); + let temp_path = temp_file.path().to_path_buf(); // do not automatically delete the file when it goes out of scope let (file, path) = temp_file.keep()?; // close the file handle to allow sshd to read it drop(file); - debug!("temporary file created at: {}", temp_path); + debug!("{}", t!("util.tempFileCreated", path = temp_path.display())); let args = Some( SshdCommandArgs { - filepath: Some(temp_path.clone()), + filepath: Some(temp_path), additional_args: None, } ); @@ -100,7 +116,7 @@ pub fn extract_sshd_defaults() -> Result, SshdConfigError> { // Clean up the temporary file regardless of success or failure let output = invoke_sshd_config_validation(args); if let Err(e) = std::fs::remove_file(&path) { - debug!("Failed to clean up temporary file {}: {}", path.display(), e); + debug!("{}", t!("util.cleanupFailed", path = path.display(), error = e)); } let result = output?; let sshd_config: Map = parse_text_to_map(&result)?; @@ -115,30 +131,24 @@ pub fn extract_sshd_defaults() -> Result, SshdConfigError> { pub fn build_command_info(input: Option<&String>, is_get: bool) -> Result { if let Some(inputs) = input { let mut sshd_config: Map = serde_json::from_str(inputs.as_str())?; + let clobber = get_bool_or_default(&mut sshd_config, "_clobber", false)?; + let include_defaults = get_bool_or_default(&mut sshd_config, "_includeDefaults", is_get)?; let metadata: Metadata = if let Some(value) = sshd_config.remove("_metadata") { serde_json::from_value(value)? } else { Metadata::new() }; - let sshd_args = metadata.filepath.as_ref().map(|filepath| { + let sshd_args = metadata.filepath.clone().map(|filepath| { SshdCommandArgs { - filepath: Some(filepath.clone()), + filepath: Some(filepath), additional_args: None, } }); - let include_defaults: bool = if let Some(value) = sshd_config.remove("_includeDefaults") { - if let Value::Bool(b) = value { - b - } else { - return Err(SshdConfigError::InvalidInput(t!("util.includeDefaultsMustBeBoolean").to_string())); - } - } else { - is_get - }; if is_get && !sshd_config.is_empty() { return Err(SshdConfigError::InvalidInput(t!("util.inputMustBeEmpty").to_string())); } return Ok(CommandInfo { + clobber, include_defaults, input: sshd_config, metadata, @@ -152,25 +162,17 @@ pub fn build_command_info(input: Option<&String>, is_get: bool) -> Result) -> Result { - let sshd_config_path = if let Some(input) = input { - input - } else if cfg!(windows) { - let program_data = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into()); - format!("{program_data}\\ssh\\sshd_config") - } else { - "/etc/ssh/sshd_config".to_string() - }; - let filepath = Path::new(&sshd_config_path); +pub fn read_sshd_config(input: Option) -> Result { + let filepath = get_default_sshd_config_path(input); if filepath.exists() { let mut sshd_config_content = String::new(); - if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(filepath) { + if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(&filepath) { use std::io::Read; file.read_to_string(&mut sshd_config_content) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; @@ -182,3 +184,26 @@ pub fn read_sshd_config(input: Option) -> Result, key: &str, default: bool) -> Result { + if let Some(value) = map.remove(key) { + if let Value::Bool(b) = value { + Ok(b) + } else { + Err(SshdConfigError::InvalidInput(t!("util.inputMustBeBoolean", input = key).to_string())) + } + } else { + Ok(default) + } +} diff --git a/resources/sshdconfig/sshd-windows.dsc.resource.json b/resources/sshdconfig/sshd-windows.dsc.resource.json index 574dc28c6..6a92d0a12 100644 --- a/resources/sshdconfig/sshd-windows.dsc.resource.json +++ b/resources/sshdconfig/sshd-windows.dsc.resource.json @@ -18,6 +18,8 @@ "executable": "sshdconfig", "args": [ "set", + "-s", + "windows-global", { "jsonInputArg": "--input", "mandatory": true diff --git a/resources/sshdconfig/tests/defaultshell.tests.ps1 b/resources/sshdconfig/tests/defaultshell.tests.ps1 index 660620b53..2135057d7 100644 --- a/resources/sshdconfig/tests/defaultshell.tests.ps1 +++ b/resources/sshdconfig/tests/defaultshell.tests.ps1 @@ -116,7 +116,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated escapeArguments = $false } | ConvertTo-Json - sshdconfig set --input $inputConfig 2>$null + sshdconfig set --input $inputConfig -s windows-global 2>$null $LASTEXITCODE | Should -Be 0 $defaultShell = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue @@ -134,7 +134,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated shell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" } | ConvertTo-Json - sshdconfig set --input $inputConfig 2>$null + sshdconfig set --input $inputConfig -s windows-global 2>$null $LASTEXITCODE | Should -Be 0 $defaultShell = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue @@ -144,7 +144,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated It 'Should handle invalid JSON input gracefully' { $invalidJson = "{ invalid json }" - sshdconfig set --input $invalidJson 2>$null + sshdconfig set --input $invalidJson -s windows-global 2>$null $LASTEXITCODE | Should -Not -Be 0 } @@ -153,7 +153,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated $inputConfig = @{ shell = "" } | ConvertTo-Json - sshdconfig set --input $inputConfig 2>$null + sshdconfig set --input $inputConfig -s windows-global 2>$null $LASTEXITCODE | Should -Be 0 $result = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue @@ -170,7 +170,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated } $inputJson = $originalConfig | ConvertTo-Json - sshdconfig set --input $inputJson 2>$null + sshdconfig set --input $inputJson -s windows-global 2>$null $LASTEXITCODE | Should -Be 0 $getOutput = sshdconfig get -s windows-global 2>$null @@ -191,7 +191,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated $inputConfig = @{ shell = $null } | ConvertTo-Json - sshdconfig set --input $inputConfig 2>$null + sshdconfig set --input $inputConfig -s windows-global 2>$null $LASTEXITCODE | Should -Be 0 $result = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue @@ -204,7 +204,7 @@ Describe 'Default Shell Configuration Error Handling on Non-Windows Platforms' - It 'Should return error for set command' { $inputConfig = @{ shell = $null } | ConvertTo-Json - $out = sshdconfig set --input $inputConfig 2>&1 + $out = sshdconfig set --input $inputConfig -s windows-global 2>&1 $LASTEXITCODE | Should -Not -Be 0 $result = $out | ConvertFrom-Json $found = $false diff --git a/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 b/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 new file mode 100644 index 000000000..756fa7160 --- /dev/null +++ b/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 @@ -0,0 +1,185 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeDiscovery { + if ($IsWindows) { + $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [System.Security.Principal.WindowsPrincipal]::new($identity) + $isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} + +Describe 'sshd_config Set Tests' -Skip:(!$isElevated) { + BeforeAll { + # Create a temporary test directory for sshd_config files + $TestDir = Join-Path $TestDrive "sshd_test" + New-Item -Path $TestDir -ItemType Directory -Force | Out-Null + $TestConfigPath = Join-Path $TestDir "sshd_config" + } + + AfterEach { + # Clean up test config file after each test + if (Test-Path $TestConfigPath) { + Remove-Item -Path $TestConfigPath -Force -ErrorAction SilentlyContinue + } + if (Test-Path "$TestConfigPath.bak") { + Remove-Item -Path "$TestConfigPath.bak" -Force -ErrorAction SilentlyContinue + } + } + + Context 'Set with valid keyword and value' { + It 'Should set a valid keyword with valid value' { + $inputConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + Port = "1234" + } | ConvertTo-Json + + $output = sshdconfig set --input $inputConfig -s sshd-config 2>$null + $LASTEXITCODE | Should -Be 0 + + # Verify file was created + Test-Path $TestConfigPath | Should -Be $true + + # Verify content using get + $getInput = @{ + _metadata = @{ + filepath = $TestConfigPath + } + } | ConvertTo-Json + $result = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json + $result.Port | Should -Be "1234" + } + + It 'Should create backup when file exists and is not managed by DSC' { + # Create a non-DSC managed file + "Port 22`nPermitRootLogin yes" | Set-Content $TestConfigPath + + $inputConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + Port = "5555" + } | ConvertTo-Json + + sshdconfig set --input $inputConfig -s sshd-config 2>$null + $LASTEXITCODE | Should -Be 0 + + # Verify backup was created + Test-Path "$TestConfigPath.bak" | Should -Be $true + + # Verify backup content + $backupContent = Get-Content "$TestConfigPath.bak" -Raw + $backupContent | Should -Match "Port 22" + $backupContent | Should -Match "PermitRootLogin yes" + + # Verify new content using get + $getInput = @{ + _metadata = @{ + filepath = $TestConfigPath + } + } | ConvertTo-Json + $result = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json + $result.Port | Should -Be "5555" + } + + It 'Should not create backup when file is already managed by DSC' { + # Create a DSC-managed file + $initialConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + Port = "6789" + } | ConvertTo-Json + + sshdconfig set --input $initialConfig -s sshd-config 2>$null + + # Update the file + $newConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + Port = "7777" + } | ConvertTo-Json + + sshdconfig set --input $newConfig -s sshd-config 2>$null + $LASTEXITCODE | Should -Be 0 + + # Verify no backup was created + Test-Path "$TestConfigPath.bak" | Should -Be $false + + # Verify content using get + $getInput = @{ + _metadata = @{ + filepath = $TestConfigPath + } + } | ConvertTo-Json + $result = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json + $result.Port | Should -Be "7777" + } + } + + Context 'Set with invalid configuration' { + It 'Should fail with clobber set to false' { + $inputConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $false + Port = "8888" + } | ConvertTo-Json + + $logFile = Join-Path $TestDrive "clobber_error.log" + sshdconfig set --input $inputConfig -s sshd-config 2>$logFile + $LASTEXITCODE | Should -Not -Be 0 + + # Read log file and check for error message + $logContent = Get-Content $logFile -Raw + $logContent | Should -Match "clobber=false is not yet supported" + } + + It 'Should fail with invalid keyword and not modify file' { + # Create initial file with valid config + $validConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + Port = "9999" + } | ConvertTo-Json + + sshdconfig set --input $validConfig -s sshd-config 2>$null + $LASTEXITCODE | Should -Be 0 + + # Get original content + $getInput = @{ + _metadata = @{ + filepath = $TestConfigPath + } + } | ConvertTo-Json + $originalResult = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json + + # Try to set with invalid keyword + $invalidConfig = @{ + _metadata = @{ + filepath = $TestConfigPath + } + _clobber = $true + FakeKeyword = "1234" + } | ConvertTo-Json + + $output = sshdconfig set --input $invalidConfig -s sshd-config 2>&1 + $LASTEXITCODE | Should -Not -Be 0 + + # Verify file content hasn't changed using get + $currentResult = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json + $currentResult.Port | Should -Be "9999" + $currentResult.Port | Should -Be $originalResult.Port + } + } +} From a49b7429d8d2d4f3eb816156d373c1e83a7d6cf1 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 19 Nov 2025 16:12:38 -0500 Subject: [PATCH 3/8] fix clippy --- resources/sshdconfig/src/set.rs | 19 +++++++------------ resources/sshdconfig/src/util.rs | 8 ++++---- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/resources/sshdconfig/src/set.rs b/resources/sshdconfig/src/set.rs index 2948076ac..e755f1ba4 100644 --- a/resources/sshdconfig/src/set.rs +++ b/resources/sshdconfig/src/set.rs @@ -10,6 +10,7 @@ use { use rust_i18n::t; use serde_json::{Map, Value}; +use std::{fmt::Write, string::String}; use tracing::debug; use crate::args::{DefaultShell, Setting}; @@ -29,7 +30,7 @@ pub fn invoke_set(input: &str, setting: &Setting) -> Result, debug!("{} {:?}", t!("set.settingSshdConfig").to_string(), setting); let cmd_info = build_command_info(Some(&input.to_string()), false)?; match set_sshd_config(&cmd_info) { - Ok(_) => Ok(Map::new()), + Ok(()) => Ok(Map::new()), Err(e) => Err(e), } }, @@ -40,11 +41,7 @@ pub fn invoke_set(input: &str, setting: &Setting) -> Result, debug!("{}", t!("set.defaultShellDebug", shell = format!("{:?}", default_shell))); // if default_shell.shell is Some, we should pass that into set default shell // otherwise pass in an empty string - let shell = if let Some(shell) = default_shell.shell.clone() { - shell - } else { - String::new() - }; + let shell: String = default_shell.shell.clone().unwrap_or_default(); set_default_shell(shell, default_shell.cmd_option, default_shell.escape_arguments)?; Ok(Map::new()) }, @@ -57,7 +54,9 @@ pub fn invoke_set(input: &str, setting: &Setting) -> Result, #[cfg(windows)] fn set_default_shell(shell: String, cmd_option: Option, escape_arguments: Option) -> Result<(), SshdConfigError> { debug!("{}", t!("set.settingDefaultShell")); - if !shell.is_empty() { + if shell.is_empty() { + remove_registry(DEFAULT_SHELL)?; + } else { // TODO: if shell contains quotes, we need to remove them let shell_path = Path::new(&shell); if shell_path.is_relative() && shell_path.components().any(|c| c == std::path::Component::ParentDir) { @@ -66,13 +65,9 @@ fn set_default_shell(shell: String, cmd_option: Option, escape_arguments if !shell_path.exists() { return Err(SshdConfigError::InvalidInput(t!("set.shellPathDoesNotExist", shell = shell).to_string())); } - set_registry(DEFAULT_SHELL, RegistryValueData::String(shell))?; - } else { - remove_registry(DEFAULT_SHELL)?; } - if let Some(cmd_option) = cmd_option { set_registry(DEFAULT_SHELL_CMD_OPTION, RegistryValueData::String(cmd_option.clone()))?; } else { @@ -120,7 +115,7 @@ fn set_sshd_config(cmd_info: &CommandInfo) -> Result<(), SshdConfigError> { if cmd_info.clobber { for (key, value) in &cmd_info.input { if let Some(value_str) = value.as_str() { - config_text.push_str(&format!("{} {}\n", key, value_str)); + writeln!(&mut config_text, "{key} {value_str}").unwrap(); } else { return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string())); } diff --git a/resources/sshdconfig/src/util.rs b/resources/sshdconfig/src/util.rs index 425973438..8a6f71557 100644 --- a/resources/sshdconfig/src/util.rs +++ b/resources/sshdconfig/src/util.rs @@ -35,16 +35,16 @@ pub fn enable_tracing() { } } -/// Get the sshd_config path based on the provided input +/// Get the `sshd_config` path based on the provided input /// If not provided, get default path for the current platform. -/// On Windows, uses the ProgramData environment variable. +/// On Windows, uses the `ProgramData` environment variable. /// On Unix-like systems, uses the standard path. pub fn get_default_sshd_config_path(input: Option) -> PathBuf { if let Some(path) = input { path } else if cfg!(windows) { let program_data = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into()); - PathBuf::from(format!("{}{}", program_data, SSHD_CONFIG_DEFAULT_PATH_WINDOWS)) + PathBuf::from(format!("{program_data}{SSHD_CONFIG_DEFAULT_PATH_WINDOWS}")) } else { PathBuf::from(SSHD_CONFIG_DEFAULT_PATH_UNIX) } @@ -162,7 +162,7 @@ pub fn build_command_info(input: Option<&String>, is_get: bool) -> Result Date: Wed, 19 Nov 2025 16:44:15 -0500 Subject: [PATCH 4/8] fix clippy --- resources/sshdconfig/src/metadata.rs | 2 +- resources/sshdconfig/src/set.rs | 2 +- resources/sshdconfig/src/util.rs | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/sshdconfig/src/metadata.rs b/resources/sshdconfig/src/metadata.rs index 4b8ec0956..2b34d2382 100644 --- a/resources/sshdconfig/src/metadata.rs +++ b/resources/sshdconfig/src/metadata.rs @@ -40,7 +40,7 @@ pub const REPEATABLE_KEYWORDS: [&str; 12] = [ ]; -pub const SSHD_CONFIG_HEADER: &str = "# This file is managed by Microsoft DSC sshdconfig resource."; +pub const SSHD_CONFIG_HEADER: &str = "# This file is managed by the Microsoft DSC sshdconfig resource."; pub const SSHD_CONFIG_DEFAULT_PATH_UNIX: &str = "/etc/ssh/sshd_config"; // For Windows, full path is constructed at runtime using ProgramData environment variable pub const SSHD_CONFIG_DEFAULT_PATH_WINDOWS: &str = "\\ssh\\sshd_config"; diff --git a/resources/sshdconfig/src/set.rs b/resources/sshdconfig/src/set.rs index e755f1ba4..1966f5a1a 100644 --- a/resources/sshdconfig/src/set.rs +++ b/resources/sshdconfig/src/set.rs @@ -88,7 +88,7 @@ fn set_default_shell(shell: String, cmd_option: Option, escape_arguments } #[cfg(not(windows))] -fn set_default_shell(_shell: Option, _cmd_option: Option, _escape_arguments: Option) -> Result<(), SshdConfigError> { +fn set_default_shell(_shell: String, _cmd_option: Option, _escape_arguments: Option) -> Result<(), SshdConfigError> { Err(SshdConfigError::InvalidInput(t!("get.windowsOnly").to_string())) } diff --git a/resources/sshdconfig/src/util.rs b/resources/sshdconfig/src/util.rs index 8a6f71557..5877ff218 100644 --- a/resources/sshdconfig/src/util.rs +++ b/resources/sshdconfig/src/util.rs @@ -35,9 +35,10 @@ pub fn enable_tracing() { } } -/// Get the `sshd_config` path based on the provided input -/// If not provided, get default path for the current platform. -/// On Windows, uses the `ProgramData` environment variable. +/// Get the `sshd_config` path +/// Uses the input value, if provided. +/// If input value not provided, get default path for the OS. +/// On Windows, uses the `ProgramData` environment variable and standard path. /// On Unix-like systems, uses the standard path. pub fn get_default_sshd_config_path(input: Option) -> PathBuf { if let Some(path) = input { From f4822a228be05c9619f5af58bff18bc7b6ef6b4a Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 19 Nov 2025 16:46:46 -0500 Subject: [PATCH 5/8] update test prereqs --- resources/sshdconfig/tests/sshdconfig.set.tests.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 b/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 index 756fa7160..ee716522d 100644 --- a/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 +++ b/resources/sshdconfig/tests/sshdconfig.set.tests.ps1 @@ -6,10 +6,12 @@ BeforeDiscovery { $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = [System.Security.Principal.WindowsPrincipal]::new($identity) $isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + $sshdExists = ($null -ne (Get-Command sshd -CommandType Application -ErrorAction Ignore)) + $skipTest = !$isElevated -or !$sshdExists } } -Describe 'sshd_config Set Tests' -Skip:(!$isElevated) { +Describe 'sshd_config Set Tests' -Skip:(!$IsWindows -or $skipTest) { BeforeAll { # Create a temporary test directory for sshd_config files $TestDir = Join-Path $TestDrive "sshd_test" From 5f11d4977161625a4adff5672507c5d0fb52a2b1 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 19 Nov 2025 16:59:10 -0500 Subject: [PATCH 6/8] fix i18n --- resources/sshdconfig/locales/en-us.toml | 4 ++++ resources/sshdconfig/src/set.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/sshdconfig/locales/en-us.toml b/resources/sshdconfig/locales/en-us.toml index 996feb935..ba47452d3 100644 --- a/resources/sshdconfig/locales/en-us.toml +++ b/resources/sshdconfig/locales/en-us.toml @@ -58,16 +58,20 @@ defaultShellDebug = "default_shell: %{shell}" failedToParseConfig = "failed to parse input for sshd config with error: '%{error}'" failedToParseDefaultShell = "failed to parse input for DefaultShell with error: '%{error}'" settingDefaultShell = "Setting default shell" +settingSshdConfig = "Setting sshd_config" shellPathDoesNotExist = "shell path does not exist: '%{shell}'" shellPathMustNotBeRelative = "shell path must not be relative" +sshdConfigReadFailed = "failed to read existing sshd_config file at path: '%{path}'" tempFileCreated = "temporary file created at: %{path}" validatingTempConfig = "Validating temporary sshd_config file" +valueMustBeString = "value for key '%{key}' must be a string" writingTempConfig = "Writing temporary sshd_config file" [util] cleanupFailed = "Failed to clean up temporary file %{path}: %{error}" inputMustBeBoolean = "value of '%{input}' must be true or false" inputWillBeIgnored = "get command does not support filtering, ignoring input" +inputMustBeEmpty = "get command does not support filtering based on input settings" sshdConfigNotFound = "sshd_config not found at path: '%{path}'" sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'" sshdElevation = "elevated security context required" diff --git a/resources/sshdconfig/src/set.rs b/resources/sshdconfig/src/set.rs index 1966f5a1a..088ef3f12 100644 --- a/resources/sshdconfig/src/set.rs +++ b/resources/sshdconfig/src/set.rs @@ -35,7 +35,7 @@ pub fn invoke_set(input: &str, setting: &Setting) -> Result, } }, Setting::WindowsGlobal => { - debug!("{} {:?}", t!("set.settingWindowsGlobal").to_string(), setting); + debug!("{} {:?}", t!("set.settingDefaultShell").to_string(), setting); match serde_json::from_str::(input) { Ok(default_shell) => { debug!("{}", t!("set.defaultShellDebug", shell = format!("{:?}", default_shell))); From cce978aef53e4e05d516d01ddf274704a471d36e Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 19 Nov 2025 17:04:57 -0500 Subject: [PATCH 7/8] address copilot feedback --- resources/sshdconfig/locales/en-us.toml | 1 + resources/sshdconfig/src/error.rs | 2 ++ resources/sshdconfig/src/set.rs | 5 ++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/sshdconfig/locales/en-us.toml b/resources/sshdconfig/locales/en-us.toml index ba47452d3..8d38ec0ee 100644 --- a/resources/sshdconfig/locales/en-us.toml +++ b/resources/sshdconfig/locales/en-us.toml @@ -8,6 +8,7 @@ setInput = "input to set in sshd_config" [error] command = "Command" invalidInput = "Invalid Input" +fmt = "Format" io = "IO" json = "JSON" language = "Language" diff --git a/resources/sshdconfig/src/error.rs b/resources/sshdconfig/src/error.rs index 74ea0ab18..65b422a6e 100644 --- a/resources/sshdconfig/src/error.rs +++ b/resources/sshdconfig/src/error.rs @@ -9,6 +9,8 @@ use thiserror::Error; pub enum SshdConfigError { #[error("{t}: {0}", t = t!("error.command"))] CommandError(String), + #[error("{t}: {0}", t = t!("error.fmt"))] + FmtError(#[from] std::fmt::Error), #[error("{t}: {0}", t = t!("error.invalidInput"))] InvalidInput(String), #[error("{t}: {0}", t = t!("error.io"))] diff --git a/resources/sshdconfig/src/set.rs b/resources/sshdconfig/src/set.rs index 088ef3f12..50a2f901b 100644 --- a/resources/sshdconfig/src/set.rs +++ b/resources/sshdconfig/src/set.rs @@ -115,13 +115,12 @@ fn set_sshd_config(cmd_info: &CommandInfo) -> Result<(), SshdConfigError> { if cmd_info.clobber { for (key, value) in &cmd_info.input { if let Some(value_str) = value.as_str() { - writeln!(&mut config_text, "{key} {value_str}").unwrap(); + writeln!(&mut config_text, "{key} {value_str}")?; } else { return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string())); } } - } - else { + } else { /* TODO: preserve existing settings that are not in input, probably need to call get */ return Err(SshdConfigError::InvalidInput(t!("set.clobberFalseUnsupported").to_string())); } From 0de026cf35d99f25e083be8042b451804ad84bc0 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Thu, 20 Nov 2025 09:42:01 -0500 Subject: [PATCH 8/8] fix i18n Removed unsupported input message for sshd config. --- resources/sshdconfig/locales/en-us.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/sshdconfig/locales/en-us.toml b/resources/sshdconfig/locales/en-us.toml index 8d38ec0ee..b4421521c 100644 --- a/resources/sshdconfig/locales/en-us.toml +++ b/resources/sshdconfig/locales/en-us.toml @@ -56,7 +56,6 @@ cleanupFailed = "Failed to clean up temporary file %{path}: %{error}" clobberFalseUnsupported = "clobber=false is not yet supported for sshd_config resource" configDoesNotExist = "sshd_config file does not exist, no backup created" defaultShellDebug = "default_shell: %{shell}" -failedToParseConfig = "failed to parse input for sshd config with error: '%{error}'" failedToParseDefaultShell = "failed to parse input for DefaultShell with error: '%{error}'" settingDefaultShell = "Setting default shell" settingSshdConfig = "Setting sshd_config" @@ -71,7 +70,6 @@ writingTempConfig = "Writing temporary sshd_config file" [util] cleanupFailed = "Failed to clean up temporary file %{path}: %{error}" inputMustBeBoolean = "value of '%{input}' must be true or false" -inputWillBeIgnored = "get command does not support filtering, ignoring input" inputMustBeEmpty = "get command does not support filtering based on input settings" sshdConfigNotFound = "sshd_config not found at path: '%{path}'" sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"