Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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