Skip to content

Commit 21da9c0

Browse files
committed
add _inheritedDefaults functionality
1 parent ea05024 commit 21da9c0

File tree

5 files changed

+98
-27
lines changed

5 files changed

+98
-27
lines changed

dsc/tests/dsc_sshdconfig.tests.ps1

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,20 @@ resources:
3030
}
3131

3232
It '<command> works' -TestCases @(
33-
@{ command = 'get'; default = $true }
34-
@{ command = 'export'; default = $false }
33+
@{ command = 'get' }
34+
@{ command = 'export' }
3535
) {
36-
param($command, $default)
36+
param($command)
3737
$out = dsc config $command -i "$yaml" | ConvertFrom-Json -Depth 10
3838
$LASTEXITCODE | Should -Be 0
3939
if ($command -eq 'export') {
4040
$out.resources.count | Should -Be 1
41-
# $out.resources[0]._includeDefaults | Should -Be $default
4241
$out.resources[0].properties | Should -Not -BeNullOrEmpty
4342
$out.resources[0].properties.port | Should -BeNullOrEmpty
4443
$out.resources[0].properties.passwordAuthentication | Should -Be 'no'
4544
$out.resources[0].properties._inheritedDefaults | Should -BeNullOrEmpty
4645
} else {
4746
$out.results.count | Should -Be 1
48-
# $out.results._includeDefaults | Should -Be $default
4947
$out.results.result.actualState | Should -Not -BeNullOrEmpty
5048
$out.results.result.actualState.port[0] | Should -Be 22
5149
$out.results.result.actualState.passwordAuthentication | Should -Be 'no'
@@ -67,10 +65,10 @@ resources:
6765
_metadata:
6866
filepath: $filepath
6967
"@
70-
$out = dsc config $command -i "$export_yaml" | ConvertFrom-Json -Depth 10
68+
$out = dsc config export -i "$export_yaml" | ConvertFrom-Json -Depth 10
7169
$LASTEXITCODE | Should -Be 0
7270
$out.resources.count | Should -Be 1
73-
($out.resources[0].properties | Measure-Object).count | Should -Be 1
71+
($out.resources[0].properties.psobject.properties | Measure-Object).count | Should -Be 1
7472
$out.resources[0].properties.passwordAuthentication | Should -Be 'no'
7573
}
7674

@@ -97,41 +95,36 @@ resources:
9795
$LASTEXITCODE | Should -Be 0
9896
if ($command -eq 'export') {
9997
$out.resources.count | Should -Be 1
100-
# $out.resources[0].metadata.includeDefaults | Should -Be $includeDefaults
101-
($out.resources[0].properties | Measure-Object).count | Should -Be 1
10298
$out.resources[0].properties.loglevel | Should -Be 'debug3'
10399
$out.resources[0].properties._inheritedDefaults | Should -Contain 'port'
104100
} else {
105101
$out.results.count | Should -Be 1
106-
# $out.results.metadata.includeDefaults | Should -Be $includeDefaults
107-
($out.results.result.actualState.psobject.properties | Measure-Object).count | Should -Be 1
102+
($out.results.result.actualState.psobject.properties | Measure-Object).count | Should -Be 2
108103
$out.results.result.actualState.loglevel | Should -Be 'debug3'
109104
$out.results.result.actualState._inheritedDefaults | Should -BeNullOrEmpty
110105
}
111106
}
112107

113-
Context 'Explicit Default Setting Behavior' {
108+
Context 'Surface a default value that has been set in file' {
114109
BeforeAll {
115110
"Port 22" | Set-Content -Path $TestDrive/test_sshd_config
116111
}
117112

118113
It '<command> works' -TestCases @(
119-
@{ command = 'get'; default = $true }
120-
@{ command = 'export'; default = $false }
114+
@{ command = 'get' }
115+
@{ command = 'export' }
121116
) {
122-
param($command, $default)
117+
param($command)
123118
$out = dsc config $command -i "$yaml" | ConvertFrom-Json -Depth 10
124119
$LASTEXITCODE | Should -Be 0
125120
if ($command -eq 'export') {
126121
$out.resources.count | Should -Be 1
127-
# $out.resources[0]._includeDefaults | Should -Be $default
128122
$out.resources[0].properties | Should -Not -BeNullOrEmpty
129123
$out.resources[0].properties.port[0] | Should -Be 22
130124
$out.resources[0].properties.passwordauthentication | Should -BeNullOrEmpty
131125
$out.resources[0].properties._inheritedDefaults | Should -BeNullOrEmpty
132126
} else {
133127
$out.results.count | Should -Be 1
134-
# $out.results._includeDefaults | Should -Be $default
135128
$out.results.result.actualState | Should -Not -BeNullOrEmpty
136129
$out.results.result.actualState.port | Should -Be 22
137130
$out.results.result.actualState.passwordAuthentication | Should -Be 'yes'

sshdconfig/locales/en-us.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,7 @@ shellPathMustNotBeRelative = "shell path must not be relative"
5454

5555
[util]
5656
inputMustBeEmpty = "get command does not support filtering based on input settings"
57+
sshdConfigNotFound = "sshd_config not found at path: '%{path}'"
58+
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
5759
sshdElevation = "elevated security context required"
5860
tracingInitError = "Failed to initialize tracing"

sshdconfig/src/get.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ use crate::args::Setting;
1616
use crate::error::SshdConfigError;
1717
use crate::inputs::CommandInfo;
1818
use crate::parser::parse_text_to_map;
19-
use crate::util::{build_command_info, extract_sshd_defaults, invoke_sshd_config_validation};
19+
use crate::util::{
20+
build_command_info,
21+
extract_sshd_defaults,
22+
invoke_sshd_config_validation,
23+
read_sshd_config
24+
};
2025

2126
/// Invoke the get command.
2227
///
@@ -93,16 +98,47 @@ fn get_default_shell() -> Result<(), SshdConfigError> {
9398
Err(SshdConfigError::InvalidInput(t!("get.windowsOnly").to_string()))
9499
}
95100

101+
/// Retrieve sshd settings.
102+
///
103+
/// # Arguments
104+
///
105+
/// * `cmd_info` - CommandInfo struct containing optional filters, metadata, and includeDefaults flag.
106+
///
107+
/// # Errors
108+
///
109+
/// This function will return an error if it cannot retrieve the sshd settings.
96110
pub fn get_sshd_settings(cmd_info: &CommandInfo) -> Result<Map<String, Value>, SshdConfigError> {
97111
let sshd_config_text = invoke_sshd_config_validation(cmd_info.sshd_args.clone())?;
98112
let mut result = parse_text_to_map(&sshd_config_text)?;
99113
let mut inherited_defaults: Vec<String> = Vec::new();
100114

101-
if !cmd_info.include_defaults {
102-
let defaults = extract_sshd_defaults()?;
103-
// Filter result based on default settings.
104-
// If a value in result is equal to the default, it will be excluded.
105-
// Note that this excludes all defaults, even if they are explicitly set in sshd_config.
115+
// parse settings from sshd_config file
116+
let sshd_config_file = read_sshd_config(cmd_info.metadata.filepath.clone())?;
117+
let explicit_settings = parse_text_to_map(&sshd_config_file)?;
118+
119+
// get default from SSHD -T with empty config
120+
let mut defaults = extract_sshd_defaults()?;
121+
122+
// remove any explicit keys from default settings list
123+
for key in explicit_settings.keys() {
124+
if defaults.contains_key(key) {
125+
defaults.remove(key);
126+
}
127+
}
128+
129+
if cmd_info.include_defaults {
130+
// Update inherited_defaults with any keys that are not explicitly set
131+
// check result for any keys that are in defaults
132+
for (key, value) in &result {
133+
if let Some(default) = defaults.get(key) {
134+
if default == value {
135+
inherited_defaults.push(key.clone());
136+
}
137+
}
138+
}
139+
} else {
140+
// Filter result based on default settings
141+
// If a value in result is equal to the default, it will be excluded
106142
result.retain(|key, value| {
107143
if let Some(default) = defaults.get(key) {
108144
default != value
@@ -116,13 +152,15 @@ pub fn get_sshd_settings(cmd_info: &CommandInfo) -> Result<Map<String, Value>, S
116152
// Filter result based on the keys provided in the input JSON.
117153
// If a provided key is not found in the result, its value is null.
118154
result.retain(|key, _| cmd_info.input.contains_key(key));
155+
inherited_defaults.retain(|key| cmd_info.input.contains_key(key));
119156
for key in cmd_info.input.keys() {
120157
result.entry(key.clone()).or_insert(Value::Null);
121158
}
122159
}
123160

124-
// does _metadata need to be checked if it has any value or will serde ignore during serialization?
125-
result.insert("_metadata".to_string(), serde_json::to_value(cmd_info.metadata.clone())?);
161+
if cmd_info.metadata.filepath.is_some() {
162+
result.insert("_metadata".to_string(), serde_json::to_value(cmd_info.metadata.clone())?);
163+
}
126164
if cmd_info.include_defaults {
127165
result.insert("_inheritedDefaults".to_string(), serde_json::to_value(inherited_defaults)?);
128166
}

sshdconfig/src/parser.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ fn parse_arguments_node(arg_node: tree_sitter::Node, input: &str, input_bytes: &
204204
pub fn parse_text_to_map(input: &str) -> Result<Map<String,Value>, SshdConfigError> {
205205
let mut parser = SshdConfigParser::new();
206206
parser.parse_text(input)?;
207-
Ok(parser.map)
207+
let lowercased_map = parser.map.into_iter()
208+
.map(|(k, v)| (k.to_lowercase(), v))
209+
.collect();
210+
Ok(lowercased_map)
208211
}
209212

210213
#[cfg(test)]

sshdconfig/src/util.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use rust_i18n::t;
55
use serde_json::{Map, Value};
6-
use std::process::Command;
6+
use std::{path::Path, process::Command};
77
use tracing::debug;
88
use tracing_subscriber::{EnvFilter, filter::LevelFilter, Layer, prelude::__tracing_subscriber_SubscriberExt};
99

@@ -147,3 +147,38 @@ pub fn build_command_info(input: Option<&String>, is_get: bool) -> Result<Comman
147147
}
148148
Ok(CommandInfo::new(is_get))
149149
}
150+
151+
/// Reads `sshd_config` file.
152+
///
153+
/// # Arguments
154+
///
155+
/// * `input` - Optional string with `sshd_config` filepath.
156+
///
157+
/// # Errors
158+
///
159+
/// This function will return an error if the file cannot be found or read.
160+
pub fn read_sshd_config(input: Option<String>) -> Result<String, SshdConfigError> {
161+
let sshd_config_path = if let Some(input) = input {
162+
input
163+
} else if cfg!(windows) {
164+
let program_data = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into());
165+
format!("{program_data}\\ssh\\sshd_config")
166+
} else {
167+
"/etc/ssh/sshd_config".to_string()
168+
};
169+
let filepath = Path::new(&sshd_config_path);
170+
171+
if filepath.exists() {
172+
let mut sshd_config_content = String::new();
173+
if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(filepath) {
174+
use std::io::Read;
175+
file.read_to_string(&mut sshd_config_content)
176+
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
177+
} else {
178+
return Err(SshdConfigError::CommandError(t!("util.sshdConfigReadFailed", path = filepath.display()).to_string()));
179+
}
180+
Ok(sshd_config_content)
181+
} else {
182+
Err(SshdConfigError::CommandError(t!("util.sshdConfigNotFound", path = filepath.display()).to_string()))
183+
}
184+
}

0 commit comments

Comments
 (0)