Skip to content

Commit 4573b3e

Browse files
authored
Merge pull request #1275 from tgauth/add-sshdconfig-set
Add sshdconfig set for "regular" keywords & clobber = true
2 parents 988cef2 + 852d83a commit 4573b3e

File tree

11 files changed

+406
-60
lines changed

11 files changed

+406
-60
lines changed

resources/sshdconfig/locales/en-us.toml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ setInput = "input to set in sshd_config"
77

88
[error]
99
command = "Command"
10+
envVar = "Environment Variable"
1011
invalidInput = "Invalid Input"
12+
fmt = "Format"
1113
io = "IO"
1214
json = "JSON"
1315
language = "Language"
@@ -49,14 +51,29 @@ unknownNode = "unknown node: '%{kind}'"
4951
unknownNodeType = "unknown node type: '%{node}'"
5052

5153
[set]
52-
failedToParseInput = "failed to parse input as DefaultShell with error: '%{error}'"
54+
backingUpConfig = "Backing up existing sshd_config file"
55+
backupCreated = "Backup created at: %{path}"
56+
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
57+
clobberFalseUnsupported = "clobber=false is not yet supported for sshd_config resource"
58+
configDoesNotExist = "sshd_config file does not exist, no backup created"
59+
defaultShellDebug = "default_shell: %{shell}"
60+
failedToParseDefaultShell = "failed to parse input for DefaultShell with error: '%{error}'"
61+
settingDefaultShell = "Setting default shell"
62+
settingSshdConfig = "Setting sshd_config"
5363
shellPathDoesNotExist = "shell path does not exist: '%{shell}'"
5464
shellPathMustNotBeRelative = "shell path must not be relative"
65+
sshdConfigReadFailed = "failed to read existing sshd_config file at path: '%{path}'"
66+
tempFileCreated = "temporary file created at: %{path}"
67+
validatingTempConfig = "Validating temporary sshd_config file"
68+
valueMustBeString = "value for key '%{key}' must be a string"
69+
writingTempConfig = "Writing temporary sshd_config file"
5570

5671
[util]
57-
includeDefaultsMustBeBoolean = "_includeDefaults must be true or false"
72+
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
73+
inputMustBeBoolean = "value of '%{input}' must be true or false"
5874
inputMustBeEmpty = "get command does not support filtering based on input settings"
5975
sshdConfigNotFound = "sshd_config not found at path: '%{path}'"
6076
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
6177
sshdElevation = "elevated security context required"
78+
tempFileCreated = "temporary file created at: %{path}"
6279
tracingInitError = "Failed to initialize tracing"

resources/sshdconfig/src/args.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ pub enum Command {
2424
/// Set default shell, eventually to be used for `sshd_config` and repeatable keywords
2525
Set {
2626
#[clap(short = 'i', long, help = t!("args.setInput").to_string())]
27-
input: String
27+
input: String,
28+
#[clap(short = 's', long, hide = true)]
29+
setting: Setting,
2830
},
2931
/// Export `sshd_config`, eventually to be used for repeatable keywords
3032
Export {

resources/sshdconfig/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use thiserror::Error;
99
pub enum SshdConfigError {
1010
#[error("{t}: {0}", t = t!("error.command"))]
1111
CommandError(String),
12+
#[error("{t}: {0}", t = t!("error.fmt"))]
13+
FmtError(#[from] std::fmt::Error),
1214
#[error("{t}: {0}", t = t!("error.invalidInput"))]
1315
InvalidInput(String),
1416
#[error("{t}: {0}", t = t!("error.io"))]
@@ -26,4 +28,6 @@ pub enum SshdConfigError {
2628
#[cfg(windows)]
2729
#[error("{t}: {0}", t = t!("error.registry"))]
2830
RegistryError(#[from] dsc_lib_registry::error::RegistryError),
31+
#[error("{t}: {0}", t = t!("error.envVar"))]
32+
EnvVarError(#[from] std::env::VarError),
2933
}

resources/sshdconfig/src/inputs.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
use serde::{Deserialize, Serialize};
55
use serde_json::{Map, Value};
6+
use std::path::PathBuf;
67

78
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
89
pub struct CommandInfo {
10+
#[serde(rename = "_clobber")]
11+
pub clobber: bool,
912
/// Switch to include defaults in the output
1013
#[serde(rename = "_includeDefaults")]
1114
pub include_defaults: bool,
@@ -21,6 +24,7 @@ impl CommandInfo {
2124
/// Create a new `CommandInfo` instance.
2225
pub fn new(include_defaults: bool) -> Self {
2326
Self {
27+
clobber: false,
2428
include_defaults,
2529
input: Map::new(),
2630
metadata: Metadata::new(),
@@ -33,7 +37,7 @@ impl CommandInfo {
3337
pub struct Metadata {
3438
/// Filepath for the `sshd_config` file to be processed
3539
#[serde(skip_serializing_if = "Option::is_none")]
36-
pub filepath: Option<String>
40+
pub filepath: Option<PathBuf>
3741
}
3842

3943
impl Metadata {
@@ -49,7 +53,7 @@ impl Metadata {
4953
pub struct SshdCommandArgs {
5054
/// the path to the `sshd_config` file to be processed
5155
#[serde(skip_serializing_if = "Option::is_none")]
52-
pub filepath: Option<String>,
56+
pub filepath: Option<PathBuf>,
5357
/// additional arguments to pass to the sshd -T command
5458
#[serde(rename = "additionalArgs", skip_serializing_if = "Option::is_none")]
5559
pub additional_args: Option<Vec<String>>,

resources/sshdconfig/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ fn main() {
5757
println!("{}", serde_json::to_string(&schema).unwrap());
5858
Ok(Map::new())
5959
},
60-
Command::Set { input } => {
60+
Command::Set { input, setting } => {
6161
debug!("{}", t!("main.set", input = input).to_string());
62-
invoke_set(input)
62+
invoke_set(input, setting)
6363
},
6464
};
6565

resources/sshdconfig/src/metadata.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ pub const REPEATABLE_KEYWORDS: [&str; 12] = [
4040
"subsystem"
4141
];
4242

43+
44+
pub const SSHD_CONFIG_HEADER: &str = "# This file is managed by the Microsoft.OpenSSH.SSHD/sshd_config DSC Resource";
45+
pub const SSHD_CONFIG_HEADER_VERSION: &str = concat!("# The Microsoft.OpenSSH.SSHD/sshd_config DSC Resource version is ", env!("CARGO_PKG_VERSION"));
46+
pub const SSHD_CONFIG_HEADER_WARNING: &str = "# Please do not modify manually, as any changes may be overwritten";
47+
pub const SSHD_CONFIG_DEFAULT_PATH_UNIX: &str = "/etc/ssh/sshd_config";
48+
// For Windows, full path is constructed at runtime using ProgramData environment variable
49+
pub const SSHD_CONFIG_DEFAULT_PATH_WINDOWS: &str = "\\ssh\\sshd_config";
50+
4351
#[cfg(windows)]
4452
pub mod windows {
4553
pub const REGISTRY_PATH: &str = "HKLM\\SOFTWARE\\OpenSSH";

resources/sshdconfig/src/set.rs

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,53 @@ use {
1010

1111
use rust_i18n::t;
1212
use serde_json::{Map, Value};
13+
use std::{fmt::Write, string::String};
14+
use tracing::{debug, info, warn};
1315

14-
use crate::args::DefaultShell;
16+
use crate::args::{DefaultShell, Setting};
1517
use crate::error::SshdConfigError;
18+
use crate::inputs::{CommandInfo, SshdCommandArgs};
19+
use crate::metadata::{SSHD_CONFIG_HEADER, SSHD_CONFIG_HEADER_VERSION, SSHD_CONFIG_HEADER_WARNING};
20+
use crate::util::{build_command_info, get_default_sshd_config_path, invoke_sshd_config_validation};
1621

1722
/// Invoke the set command.
1823
///
1924
/// # Errors
2025
///
2126
/// This function will return an error if the desired settings cannot be applied.
22-
pub fn invoke_set(input: &str) -> Result<Map<String, Value>, SshdConfigError> {
23-
match serde_json::from_str::<DefaultShell>(input) {
24-
Ok(default_shell) => {
25-
set_default_shell(default_shell.shell, default_shell.cmd_option, default_shell.escape_arguments)?;
26-
Ok(Map::new())
27+
pub fn invoke_set(input: &str, setting: &Setting) -> Result<Map<String, Value>, SshdConfigError> {
28+
match setting {
29+
Setting::SshdConfig => {
30+
debug!("{} {:?}", t!("set.settingSshdConfig").to_string(), setting);
31+
let cmd_info = build_command_info(Some(&input.to_string()), false)?;
32+
match set_sshd_config(&cmd_info) {
33+
Ok(()) => Ok(Map::new()),
34+
Err(e) => Err(e),
35+
}
2736
},
28-
Err(e) => {
29-
Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string()))
37+
Setting::WindowsGlobal => {
38+
debug!("{} {:?}", t!("set.settingDefaultShell").to_string(), setting);
39+
match serde_json::from_str::<DefaultShell>(input) {
40+
Ok(default_shell) => {
41+
debug!("{}", t!("set.defaultShellDebug", shell = format!("{:?}", default_shell)));
42+
// if default_shell.shell is Some, we should pass that into set default shell
43+
// otherwise pass in an empty string
44+
let shell: String = default_shell.shell.clone().unwrap_or_default();
45+
set_default_shell(shell, default_shell.cmd_option, default_shell.escape_arguments)?;
46+
Ok(Map::new())
47+
},
48+
Err(e) => Err(SshdConfigError::InvalidInput(t!("set.failedToParseDefaultShell", error = e).to_string())),
49+
}
3050
}
3151
}
3252
}
3353

3454
#[cfg(windows)]
35-
fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
36-
if let Some(shell) = shell {
55+
fn set_default_shell(shell: String, cmd_option: Option<String>, escape_arguments: Option<bool>) -> Result<(), SshdConfigError> {
56+
debug!("{}", t!("set.settingDefaultShell"));
57+
if shell.is_empty() {
58+
remove_registry(DEFAULT_SHELL)?;
59+
} else {
3760
// TODO: if shell contains quotes, we need to remove them
3861
let shell_path = Path::new(&shell);
3962
if shell_path.is_relative() && shell_path.components().any(|c| c == std::path::Component::ParentDir) {
@@ -42,13 +65,9 @@ fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_a
4265
if !shell_path.exists() {
4366
return Err(SshdConfigError::InvalidInput(t!("set.shellPathDoesNotExist", shell = shell).to_string()));
4467
}
45-
4668
set_registry(DEFAULT_SHELL, RegistryValueData::String(shell))?;
47-
} else {
48-
remove_registry(DEFAULT_SHELL)?;
4969
}
5070

51-
5271
if let Some(cmd_option) = cmd_option {
5372
set_registry(DEFAULT_SHELL_CMD_OPTION, RegistryValueData::String(cmd_option.clone()))?;
5473
} else {
@@ -69,7 +88,7 @@ fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_a
6988
}
7089

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

@@ -86,3 +105,79 @@ fn remove_registry(name: &str) -> Result<(), SshdConfigError> {
86105
registry_helper.remove()?;
87106
Ok(())
88107
}
108+
109+
fn set_sshd_config(cmd_info: &CommandInfo) -> Result<(), SshdConfigError> {
110+
// this should be its own helper function that checks that the value makes sense for the key type
111+
// i.e. if the key can be repeated or have multiple values, etc.
112+
// or if the value is something besides a string (like an object to convert back into a comma-separated list)
113+
debug!("{}", t!("set.writingTempConfig"));
114+
let mut config_text = SSHD_CONFIG_HEADER.to_string() + "\n" + SSHD_CONFIG_HEADER_VERSION + "\n" + SSHD_CONFIG_HEADER_WARNING + "\n";
115+
if cmd_info.clobber {
116+
for (key, value) in &cmd_info.input {
117+
if let Some(value_str) = value.as_str() {
118+
writeln!(&mut config_text, "{key} {value_str}")?;
119+
} else {
120+
return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string()));
121+
}
122+
}
123+
} else {
124+
/* TODO: preserve existing settings that are not in input, probably need to call get */
125+
return Err(SshdConfigError::InvalidInput(t!("set.clobberFalseUnsupported").to_string()));
126+
}
127+
128+
// Write input to a temporary file and validate it with SSHD -T
129+
let temp_file = tempfile::Builder::new()
130+
.prefix("sshd_config_temp_")
131+
.suffix(".tmp")
132+
.tempfile()?;
133+
let temp_path = temp_file.path().to_path_buf();
134+
let (file, path) = temp_file.keep()?;
135+
debug!("{}", t!("set.tempFileCreated", path = temp_path.display()));
136+
std::fs::write(&temp_path, &config_text)
137+
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
138+
drop(file);
139+
140+
let args = Some(
141+
SshdCommandArgs {
142+
filepath: Some(temp_path),
143+
additional_args: None,
144+
}
145+
);
146+
147+
debug!("{}", t!("set.validatingTempConfig"));
148+
let result = invoke_sshd_config_validation(args);
149+
// Always cleanup temp file, regardless of result success or failure
150+
if let Err(e) = std::fs::remove_file(&path) {
151+
warn!("{}", t!("set.cleanupFailed", path = path.display(), error = e));
152+
}
153+
// Propagate failure, if any
154+
result?;
155+
156+
let sshd_config_path = get_default_sshd_config_path(cmd_info.metadata.filepath.clone())?;
157+
158+
if sshd_config_path.exists() {
159+
let mut sshd_config_content = String::new();
160+
if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(&sshd_config_path) {
161+
use std::io::Read;
162+
file.read_to_string(&mut sshd_config_content)
163+
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
164+
} else {
165+
return Err(SshdConfigError::CommandError(t!("set.sshdConfigReadFailed", path = sshd_config_path.display()).to_string()));
166+
}
167+
if !sshd_config_content.starts_with(SSHD_CONFIG_HEADER) {
168+
// If config file is not already managed by this resource, create a backup of the existing file
169+
debug!("{}", t!("set.backingUpConfig"));
170+
let backup_path = format!("{}_backup", sshd_config_path.display());
171+
std::fs::write(&backup_path, &sshd_config_content)
172+
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
173+
info!("{}", t!("set.backupCreated", path = backup_path));
174+
}
175+
} else {
176+
debug!("{}", t!("set.configDoesNotExist"));
177+
}
178+
179+
std::fs::write(&sshd_config_path, &config_text)
180+
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
181+
182+
Ok(())
183+
}

0 commit comments

Comments
 (0)