Skip to content

Commit d79460f

Browse files
authored
Merge pull request #1307 from tgauth/sshdconfig-fixes
Expand sshdconfig set capability
2 parents 9e5de33 + 3442086 commit d79460f

File tree

9 files changed

+328
-47
lines changed

9 files changed

+328
-47
lines changed

dsc/tests/dsc_sshdconfig.tests.ps1

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,44 @@ resources:
133133
}
134134
}
135135
}
136+
137+
Context 'Set Commands' {
138+
It 'Set works with _clobber: true' {
139+
$set_yaml = @"
140+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
141+
metadata:
142+
Microsoft.DSC:
143+
securityContext: elevated
144+
resources:
145+
- name: sshdconfig
146+
type: Microsoft.OpenSSH.SSHD/sshd_config
147+
metadata:
148+
filepath: $filepath
149+
properties:
150+
_clobber: true
151+
port: 1234
152+
allowUsers:
153+
- user1
154+
- user2
155+
passwordAuthentication: $false
156+
ciphers:
157+
- aes128-ctr
158+
- aes192-ctr
159+
- aes256-ctr
160+
addressFamily: inet6
161+
authorizedKeysFile:
162+
- ./.ssh/authorized_keys
163+
- ./.ssh/authorized_keys2
164+
"@
165+
$out = dsc config set -i "$set_yaml" | ConvertFrom-Json -Depth 10
166+
$LASTEXITCODE | Should -Be 0
167+
$out.results.count | Should -Be 1
168+
$out.results.result.afterState.port | Should -Be 1234
169+
$out.results.result.afterState.passwordauthentication | Should -Be $false
170+
$out.results.result.afterState.ciphers | Should -Be @('aes128-ctr', 'aes192-ctr', 'aes256-ctr')
171+
$out.results.result.afterState.allowusers | Should -Be @('user1', 'user2')
172+
$out.results.result.afterState.addressfamily | Should -Be 'inet6'
173+
$out.results.result.afterState.authorizedkeysfile | Should -Be @('./.ssh/authorized_keys', './.ssh/authorized_keys2')
174+
}
175+
}
136176
}

resources/sshdconfig/locales/en-us.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ windowsOnly = "Microsoft.OpenSSH.SSHD/Windows is only applicable to Windows"
3030

3131
[main]
3232
export = "Export command: %{input}"
33+
invalidTraceLevel = "Invalid trace level"
3334
schema = "Schema command:"
3435
set = "Set command: '%{input}'"
3536

@@ -67,13 +68,15 @@ shellPathMustNotBeRelative = "shell path must not be relative"
6768
sshdConfigReadFailed = "failed to read existing sshd_config file at path: '%{path}'"
6869
tempFileCreated = "temporary file created at: %{path}"
6970
validatingTempConfig = "Validating temporary sshd_config file"
70-
valueMustBeString = "value for key '%{key}' must be a string"
7171
writingTempConfig = "Writing temporary sshd_config file"
7272

7373
[util]
7474
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
75+
deserializeFailed = "Failed to deserialize match input: %{error}"
76+
getIgnoresInputFilters = "get command does not support filtering based on input settings, provided input will be ignored"
7577
inputMustBeBoolean = "value of '%{input}' must be true or false"
76-
inputMustBeEmpty = "get command does not support filtering based on input settings"
78+
invalidValue = "Key: '%{key}' cannot have empty value"
79+
matchBlockMissingCriteria = "Match block must contain 'criteria' field"
7780
sshdConfigNotFound = "sshd_config not found at path: '%{path}'"
7881
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
7982
sshdElevation = "elevated security context required"

resources/sshdconfig/src/args.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,30 @@ use rust_i18n::t;
66
use schemars::JsonSchema;
77
use serde::{Deserialize, Serialize};
88

9+
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
10+
pub enum TraceFormat {
11+
Default,
12+
Plaintext,
13+
Json,
14+
}
15+
16+
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
17+
pub enum TraceLevel {
18+
Error,
19+
Warn,
20+
Info,
21+
Debug,
22+
Trace
23+
}
24+
925
#[derive(Parser)]
1026
pub struct Args {
1127
#[clap(subcommand)]
1228
pub command: Command,
29+
#[clap(short = 'l', long, help = "Trace level to use", value_enum)]
30+
pub trace_level: Option<TraceLevel>,
31+
#[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "json")]
32+
pub trace_format: TraceFormat,
1333
}
1434

1535
#[derive(Subcommand)]

resources/sshdconfig/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ const EXIT_SUCCESS: i32 = 0;
2929
const EXIT_FAILURE: i32 = 1;
3030

3131
fn main() {
32-
enable_tracing();
33-
3432
let args = Args::parse();
3533

34+
enable_tracing(args.trace_level.as_ref(), &args.trace_format);
35+
3636
let result = match &args.command {
3737
Command::Export { input } => {
3838
debug!("{}: {:?}", t!("main.export").to_string(), input);

resources/sshdconfig/src/metadata.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
// note that it is possible for a keyword to be in one, neither, or both of the multi-arg and repeatable lists below.
4+
// the multi-arg comma-separated and space-separated lists are mutually exclusive, but the repeatable list can overlap with either of them.
5+
// the multi-arg lists are maintained for formatting arrays into the correct format when writing back to the config file.
56

67
// keywords that can have multiple comma-separated arguments per line and should be represented as arrays.
7-
pub const MULTI_ARG_KEYWORDS: [&str; 22] = [
8-
"acceptenv",
9-
"allowgroups",
10-
"allowusers",
8+
pub const MULTI_ARG_KEYWORDS_COMMA_SEP: [&str; 11] = [
119
"authenticationmethods",
12-
"authorizedkeysfile",
1310
"casignaturealgorithms",
14-
"channeltimeout",
1511
"ciphers",
16-
"denygroups",
17-
"denyusers",
1812
"hostbasedacceptedalgorithms",
1913
"hostkeyalgorithms",
20-
"ipqos",
2114
"kexalgorithms",
2215
"macs",
23-
"permitlisten",
24-
"permitopen",
2516
"permituserenvironment",
26-
"persourcepenalties",
2717
"persourcepenaltyexemptlist",
2818
"pubkeyacceptedalgorithms",
2919
"rekeylimit" // first arg is bytes, second arg (optional) is amount of time
3020
];
3121

22+
// keywords that can have multiple space-separated arguments per line and should be represented as arrays.
23+
pub const MULTI_ARG_KEYWORDS_SPACE_SEP: [&str; 11] = [
24+
"acceptenv",
25+
"allowgroups",
26+
"allowusers",
27+
"authorizedkeysfile",
28+
"channeltimeout",
29+
"denygroups",
30+
"denyusers",
31+
"ipqos",
32+
"permitlisten",
33+
"permitopen",
34+
"persourcepenalties",
35+
];
36+
3237
// keywords that can be repeated over multiple lines and should be represented as arrays.
3338
pub const REPEATABLE_KEYWORDS: [&str; 12] = [
3439
"acceptenv",
@@ -45,7 +50,6 @@ pub const REPEATABLE_KEYWORDS: [&str; 12] = [
4550
"subsystem"
4651
];
4752

48-
4953
pub const SSHD_CONFIG_HEADER: &str = "# This file is managed by the Microsoft.OpenSSH.SSHD/sshd_config DSC Resource";
5054
pub const SSHD_CONFIG_HEADER_VERSION: &str = concat!("# The Microsoft.OpenSSH.SSHD/sshd_config DSC Resource version is ", env!("CARGO_PKG_VERSION"));
5155
pub const SSHD_CONFIG_HEADER_WARNING: &str = "# Please do not modify manually, as any changes may be overwritten";

resources/sshdconfig/src/parser.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::debug;
88
use tree_sitter::Parser;
99

1010
use crate::error::SshdConfigError;
11-
use crate::metadata::{MULTI_ARG_KEYWORDS, REPEATABLE_KEYWORDS};
11+
use crate::metadata::{MULTI_ARG_KEYWORDS_COMMA_SEP, MULTI_ARG_KEYWORDS_SPACE_SEP, REPEATABLE_KEYWORDS};
1212

1313
#[derive(Debug, JsonSchema)]
1414
pub struct SshdConfigParser {
@@ -147,9 +147,9 @@ impl SshdConfigParser {
147147
let Ok(text) = keyword.utf8_text(input_bytes) else {
148148
return Err(SshdConfigError::ParserError(t!("parser.failedToParseNode", input = input).to_string()));
149149
};
150-
151-
is_repeatable = REPEATABLE_KEYWORDS.contains(&text);
152-
is_vec = is_repeatable || MULTI_ARG_KEYWORDS.contains(&text);
150+
let lowercase_key = text.to_lowercase();
151+
is_repeatable = REPEATABLE_KEYWORDS.contains(&lowercase_key.as_str());
152+
is_vec = is_repeatable || MULTI_ARG_KEYWORDS_COMMA_SEP.contains(&lowercase_key.as_str()) || MULTI_ARG_KEYWORDS_SPACE_SEP.contains(&lowercase_key.as_str());
153153
key = Some(text.to_string());
154154
}
155155

@@ -467,12 +467,6 @@ match user testuser
467467
let result: Map<String, Value> = parse_text_to_map(input).unwrap();
468468
let match_array = result.get("match").unwrap().as_array().unwrap();
469469
let match_obj = match_array[0].as_object().unwrap();
470-
for (k, v) in match_obj.iter() {
471-
eprintln!(" {}: {:?}", k, v);
472-
}
473-
474-
// allowgroups is both MULTI_ARG and REPEATABLE
475-
// Space-separated values should be parsed as array
476470
let allowgroups = match_obj.get("allowgroups").unwrap().as_array().unwrap();
477471
assert_eq!(allowgroups.len(), 2);
478472
assert_eq!(allowgroups[0], Value::String("administrators".to_string()));

resources/sshdconfig/src/set.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use tracing::{debug, info, warn};
1616
use crate::args::{DefaultShell, Setting};
1717
use crate::error::SshdConfigError;
1818
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};
19+
use crate::metadata::{REPEATABLE_KEYWORDS, SSHD_CONFIG_HEADER, SSHD_CONFIG_HEADER_VERSION, SSHD_CONFIG_HEADER_WARNING};
20+
use crate::util::{build_command_info, format_sshd_value, get_default_sshd_config_path, invoke_sshd_config_validation};
2121

2222
/// Invoke the set command.
2323
///
@@ -114,10 +114,24 @@ fn set_sshd_config(cmd_info: &CommandInfo) -> Result<(), SshdConfigError> {
114114
let mut config_text = SSHD_CONFIG_HEADER.to_string() + "\n" + SSHD_CONFIG_HEADER_VERSION + "\n" + SSHD_CONFIG_HEADER_WARNING + "\n";
115115
if cmd_info.clobber {
116116
for (key, value) in &cmd_info.input {
117-
if let Some(value_str) = value.as_str() {
118-
writeln!(&mut config_text, "{key} {value_str}")?;
117+
let key_lower = key.to_lowercase();
118+
119+
// Handle repeatable keywords - write multiple lines
120+
if REPEATABLE_KEYWORDS.contains(&key_lower.as_str()) {
121+
if let Value::Array(arr) = value {
122+
for item in arr {
123+
let formatted = format_sshd_value(key, item)?;
124+
writeln!(&mut config_text, "{key} {formatted}")?;
125+
}
126+
} else {
127+
// Single value for repeatable keyword, write as-is
128+
let formatted = format_sshd_value(key, value)?;
129+
writeln!(&mut config_text, "{key} {formatted}")?;
130+
}
119131
} else {
120-
return Err(SshdConfigError::InvalidInput(t!("set.valueMustBeString", key = key).to_string()));
132+
// Handle non-repeatable keywords - format and write single line
133+
let formatted = format_sshd_value(key, value)?;
134+
writeln!(&mut config_text, "{key} {formatted}")?;
121135
}
122136
}
123137
} else {

0 commit comments

Comments
 (0)