-
Notifications
You must be signed in to change notification settings - Fork 55
Add sshdconfig _purge=false support for regular keywords #1348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SteveL-MSFT
merged 17 commits into
PowerShell:main
from
tgauth:add-sshdconfig-clobber-false
Jan 14, 2026
+471
−198
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
9523b4c
add _clobber=false tests
tgauth 353b1d3
update tests
tgauth 6cbeed5
Merge branch 'add-sshdconfig-match-to-export' into add-sshdconfig-clo…
tgauth 53ca91e
add support for regular keywords and _clobber=false
tgauth 5e8daab
rename _clobber to _purge
tgauth 964ce3d
display error message when file does not exist but _purge=false
tgauth 92396c7
remove debug statement
tgauth 4329686
modify order preservation test
tgauth 8db613b
Merge remote-tracking branch 'upstream/main' into add-sshdconfig-clob…
tgauth 44817c8
fix i8n
tgauth a2a86f0
Apply suggestions from Copilot
tgauth 384ca7a
fix test
tgauth 459c6ee
lowercase input keys as part of CommandInfo initialization
tgauth 402fde0
tweak match block writing
tgauth 74a0915
add formatter mod with struct and display implemented
tgauth f39a38e
address copilot feedback
tgauth 51a909b
Add TODO comment
tgauth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use rust_i18n::t; | ||
| use serde::Deserialize; | ||
| use serde_json::{Map, Value}; | ||
| use std::{fmt, fmt::Write}; | ||
| use tracing::warn; | ||
|
|
||
| use crate::error::SshdConfigError; | ||
| use crate::metadata::{MULTI_ARG_KEYWORDS_COMMA_SEP, REPEATABLE_KEYWORDS}; | ||
|
|
||
| #[derive(Debug, Deserialize)] | ||
| struct MatchBlock { | ||
| criteria: Map<String, Value>, | ||
| #[serde(flatten)] | ||
| contents: Map<String, Value>, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug)] | ||
| pub struct SshdConfigValue<'a> { | ||
| is_repeatable: bool, | ||
| key: &'a str, | ||
| separator: ValueSeparator, | ||
| value: &'a Value, | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug)] | ||
| pub enum ValueSeparator { | ||
| Comma, | ||
| Space, | ||
| } | ||
|
|
||
| impl<'a> SshdConfigValue<'a> { | ||
| /// Create a new SSHD config value, returning an error if the value is empty/invalid | ||
| pub fn try_new(key: &'a str, value: &'a Value, override_separator: Option<ValueSeparator>) -> Result<Self, SshdConfigError> { | ||
| if matches!(value, Value::Null | Value::Object(_)) { | ||
| return Err(SshdConfigError::ParserError( | ||
| t!("formatter.invalidValue", key = key).to_string() | ||
| )); | ||
| } | ||
|
|
||
| if let Value::Array(arr) = value { | ||
| if arr.is_empty() { | ||
| return Err(SshdConfigError::ParserError( | ||
| t!("formatter.invalidValue", key = key).to_string() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| let separator = match override_separator { | ||
| Some(separator) => separator, | ||
| None => { | ||
| if MULTI_ARG_KEYWORDS_COMMA_SEP.contains(&key) { | ||
| ValueSeparator::Comma | ||
| } else { | ||
| ValueSeparator::Space | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| let is_repeatable = REPEATABLE_KEYWORDS.contains(&key); | ||
|
|
||
| Ok(Self { | ||
| is_repeatable, | ||
| key, | ||
| separator, | ||
| value, | ||
| }) | ||
| } | ||
|
|
||
| pub fn write_to_config(&self, config_text: &mut String) -> Result<(), SshdConfigError> { | ||
| if self.is_repeatable { | ||
| if let Value::Array(arr) = self.value { | ||
| for item in arr { | ||
| let item = SshdConfigValue::try_new(self.key, item, Some(self.separator))?; | ||
| writeln!(config_text, "{} {item}", self.key)?; | ||
| } | ||
| } else { | ||
| writeln!(config_text, "{} {self}", self.key)?; | ||
| } | ||
| } else { | ||
| writeln!(config_text, "{} {self}", self.key)?; | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl fmt::Display for SshdConfigValue<'_> { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| match self.value { | ||
| Value::Array(arr) => { | ||
| if arr.is_empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let separator = match self.separator { | ||
| ValueSeparator::Comma => ",", | ||
| ValueSeparator::Space => " ", | ||
| }; | ||
|
|
||
| let mut first = true; | ||
| for item in arr { | ||
| if let Ok(sshd_config_value) = SshdConfigValue::try_new(self.key, item, Some(self.separator)) { | ||
| let formatted = sshd_config_value.to_string(); | ||
| if !formatted.is_empty() { | ||
| if !first { | ||
| write!(f, "{separator}")?; | ||
| } | ||
| write!(f, "{formatted}")?; | ||
| first = false; | ||
| } | ||
| } else { | ||
| warn!("{}", t!("formatter.invalidArrayItem", key = self.key, item = item).to_string()); | ||
| } | ||
| } | ||
| Ok(()) | ||
| }, | ||
| Value::Bool(b) => write!(f, "{}", if *b { "yes" } else { "no" }), | ||
| Value::Number(n) => write!(f, "{n}"), | ||
| Value::String(s) => { | ||
| if s.contains(char::is_whitespace) { | ||
| write!(f, "\"{s}\"") | ||
| } else { | ||
| write!(f, "{s}") | ||
| } | ||
| }, | ||
| Value::Null | Value::Object(_) => Ok(()), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn format_match_block(match_obj: &Value) -> Result<String, SshdConfigError> { | ||
| let match_block = match serde_json::from_value::<MatchBlock>(match_obj.clone()) { | ||
| Ok(result) => { | ||
| result | ||
| } | ||
| Err(e) => { | ||
| return Err(SshdConfigError::ParserError(t!("formatter.deserializeFailed", error = e).to_string())); | ||
| } | ||
| }; | ||
|
|
||
| if match_block.criteria.is_empty() { | ||
| return Err(SshdConfigError::InvalidInput(t!("formatter.matchBlockMissingCriteria").to_string())); | ||
| } | ||
|
|
||
| let mut match_parts = vec![]; | ||
| let mut result = vec![]; | ||
|
|
||
| for (key, value) in &match_block.criteria { | ||
| // all match criteria values are comma-separated | ||
| let sshd_config_value = SshdConfigValue::try_new(key, value, Some(ValueSeparator::Comma))?; | ||
| match_parts.push(format!("{key} {sshd_config_value}")); | ||
| } | ||
|
|
||
| // Write the Match line with the formatted criteria(s) | ||
| result.push(match_parts.join(" ")); | ||
|
|
||
| // Format other keywords in the match block | ||
| for (key, value) in &match_block.contents { | ||
| let sshd_config_value = SshdConfigValue::try_new(key, value, None)?; | ||
| result.push(format!("\t{key} {sshd_config_value}")); | ||
| } | ||
|
|
||
| Ok(result.join("\n")) | ||
| } | ||
|
|
||
| /// Write configuration map to config text string | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// This function will return an error if formatting fails. | ||
| pub fn write_config_map_to_text(global_map: &Map<String, Value>) -> Result<String, SshdConfigError> { | ||
| let match_map = global_map.get("match"); | ||
| let mut config_text = String::new(); | ||
|
|
||
| for (key, value) in global_map { | ||
| let key_lower = key.to_lowercase(); | ||
|
|
||
| if key_lower == "match" { | ||
| continue; // match blocks are handled after global settings | ||
| } | ||
|
|
||
| let sshd_config_value = SshdConfigValue::try_new(key, value, None)?; | ||
| sshd_config_value.write_to_config(&mut config_text)?; | ||
| } | ||
|
|
||
| if let Some(match_map) = match_map { | ||
| if let Value::Array(arr) = match_map { | ||
| for item in arr { | ||
| let formatted = format_match_block(item)?; | ||
| writeln!(&mut config_text, "Match {formatted}")?; | ||
| } | ||
| } else { | ||
| let formatted = format_match_block(match_map)?; | ||
| writeln!(&mut config_text, "Match {formatted}")?; | ||
| } | ||
| } | ||
|
|
||
| Ok(config_text) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.