Skip to content
20 changes: 18 additions & 2 deletions resources/sshdconfig/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -49,14 +50,29 @@ 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}"
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]
includeDefaultsMustBeBoolean = "_includeDefaults must be true or false"
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
inputMustBeBoolean = "value of '%{input}' must be true or false"
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"
tempFileCreated = "temporary file created at: %{path}"
tracingInitError = "Failed to initialize tracing"
4 changes: 3 additions & 1 deletion resources/sshdconfig/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions resources/sshdconfig/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down
8 changes: 6 additions & 2 deletions resources/sshdconfig/src/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand All @@ -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<String>
pub filepath: Option<PathBuf>
}

impl Metadata {
Expand All @@ -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<String>,
pub filepath: Option<PathBuf>,
/// additional arguments to pass to the sshd -T command
#[serde(rename = "additionalArgs", skip_serializing_if = "Option::is_none")]
pub additional_args: Option<Vec<String>>,
Expand Down
4 changes: 2 additions & 2 deletions resources/sshdconfig/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
};

Expand Down
6 changes: 6 additions & 0 deletions resources/sshdconfig/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub const REPEATABLE_KEYWORDS: [&str; 12] = [
"subsystem"
];


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";

#[cfg(windows)]
pub mod windows {
pub const REGISTRY_PATH: &str = "HKLM\\SOFTWARE\\OpenSSH";
Expand Down
125 changes: 110 additions & 15 deletions resources/sshdconfig/src/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,53 @@ use {

use rust_i18n::t;
use serde_json::{Map, Value};
use std::{fmt::Write, string::String};
use tracing::debug;

use crate::args::DefaultShell;
use crate::args::{DefaultShell, Setting};
use crate::error::SshdConfigError;
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<Map<String, Value>, SshdConfigError> {
match serde_json::from_str::<DefaultShell>(input) {
Ok(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<Map<String, Value>, 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(e) => {
Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string()))
Setting::WindowsGlobal => {
debug!("{} {:?}", t!("set.settingDefaultShell").to_string(), setting);
match serde_json::from_str::<DefaultShell>(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: String = default_shell.shell.clone().unwrap_or_default();
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())),
}
}
}
}

#[cfg(windows)]
fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
if let Some(shell) = shell {
fn set_default_shell(shell: String, cmd_option: Option<String>, escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
debug!("{}", t!("set.settingDefaultShell"));
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) {
Expand All @@ -42,13 +65,9 @@ fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_a
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 {
Expand All @@ -69,7 +88,7 @@ fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_a
}

#[cfg(not(windows))]
fn set_default_shell(_shell: Option<String>, _cmd_option: Option<String>, _escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
fn set_default_shell(_shell: String, _cmd_option: Option<String>, _escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
Err(SshdConfigError::InvalidInput(t!("get.windowsOnly").to_string()))
}

Expand All @@ -86,3 +105,79 @@ fn remove_registry(name: &str) -> Result<(), SshdConfigError> {
registry_helper.remove()?;
Ok(())
}

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() {
writeln!(&mut config_text, "{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()));
}

// 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_path_buf();
let (file, path) = temp_file.keep()?;
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(
SshdCommandArgs {
filepath: Some(temp_path),
additional_args: None,
}
);

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?;

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) {
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()));
}
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!("{}", t!("set.backupCreated", path = backup_path));
}
} else {
debug!("{}", t!("set.configDoesNotExist"));
}

std::fs::write(&sshd_config_path, &config_text)
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;

Ok(())
}
Loading