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
193 changes: 193 additions & 0 deletions dsc/tests/dsc_sshdconfig.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,197 @@ resources:
$out.results.result.afterState.authorizedkeysfile | Should -Be @('./.ssh/authorized_keys', './.ssh/authorized_keys2')
}
}

Context 'Subsystem and SubsystemList Tests' {
BeforeAll {
# Create a temporary test directory for sshd_config files
$TestDir = Join-Path $TestDrive "sshd_test"
New-Item -Path $TestDir -ItemType Directory -Force | Out-Null
$script:TestConfigPath = Join-Path $TestDir "sshd_config"

# Define OS-specific paths with spaces
if ($IsWindows) {
$script:PathWithSpaces = "C:\Program Files\OpenSSH\sftp-server.exe"
$script:DefaultSftpPath = "sftp-server.exe"
$script:AlternatePath = "C:\OpenSSH\bin\sftp.exe"
}
else {
$script:PathWithSpaces = "/usr/local/lib/openssh server/sftp-server"
$script:DefaultSftpPath = "/usr/lib/openssh/sftp-server"
$script:AlternatePath = "/usr/libexec/sftp-server"
}
}

BeforeEach {
# Create test config with existing subsystems
$initialContent = @"
Port 22
subsystem sftp $script:DefaultSftpPath
Subsystem test2 /path/to/test2
PasswordAuthentication yes
"@
Set-Content -Path $script:TestConfigPath -Value $initialContent
}

AfterEach {
# Clean up test config file after each test
if (Test-Path $script:TestConfigPath) {
Remove-Item -Path $script:TestConfigPath -Force -ErrorAction SilentlyContinue
}
if (Test-Path "$script:TestConfigPath.bak") {
Remove-Item -Path "$script:TestConfigPath.bak" -Force -ErrorAction SilentlyContinue
}
}

It 'Should add a new subsystem that does not already exist' -Skip:($script:skipSubsystemTests) {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
Microsoft.DSC:
securityContext: elevated
resources:
- name: newsub
type: Microsoft.OpenSSH.SSHD/Subsystem
metadata:
filepath: $($script:TestConfigPath -replace '\\', '/')
properties:
_exist: true
subsystem:
name: newsubsystem
value: /path/to/newsubsystem
"@
$out = dsc config set -i "$config_yaml" | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.hadErrors | Should -BeFalse
$out.results.Count | Should -Be 1
$out.results[0].type | Should -BeExactly 'Microsoft.OpenSSH.SSHD/Subsystem'
$out.results[0].result.afterState._exist | Should -Be $true
$out.results[0].result.afterState.subsystem.name | Should -Be 'newsubsystem'

# Verify file contains the new subsystem
$subsystems = Get-Content $script:TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
$subsystems.Count | Should -Be 3
$subsystems | Should -Contain "subsystem newsubsystem /path/to/newsubsystem"
}

It 'Should remove a subsystem when _exist is false' -Skip:($script:skipSubsystemTests) {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
Microsoft.DSC:
securityContext: elevated
resources:
- name: removesub
type: Microsoft.OpenSSH.SSHD/Subsystem
metadata:
filepath: $($script:TestConfigPath -replace '\\', '/')
properties:
_exist: false
subsystem:
name: sftp
"@
$out = dsc config set -i "$config_yaml" | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.hadErrors | Should -BeFalse
$out.results[0].result.afterState._exist | Should -Be $false

# Verify subsystem was removed
$subsystems = Get-Content $script:TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
$subsystems.Count | Should -Be 1
$subsystems | Should -Not -Match 'sftp'
}

It 'Should add multiple new subsystems with SubsystemList' -Skip:($script:skipSubsystemTests) {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
Microsoft.DSC:
securityContext: elevated
resources:
- name: multisubsystem
type: Microsoft.OpenSSH.SSHD/SubsystemList
metadata:
filepath: $script:TestConfigPath
properties:
_purge: false
subsystem:
- name: newsub1
value: /path/to/newsub1
- name: newsub2
value: /path/to/newsub2
"@
$out = dsc config set -i "$config_yaml" | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.hadErrors | Should -BeFalse
$out.results[0].type | Should -BeExactly 'Microsoft.OpenSSH.SSHD/SubsystemList'
$out.results[0].result.afterState.subsystem.Count | Should -Be 4

# Verify all subsystems are present (old + new)
$subsystems = Get-Content $script:TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
$subsystems.Count | Should -Be 4
$subsystems | Should -Contain "subsystem newsub1 /path/to/newsub1"
$subsystems | Should -Contain "subsystem newsub2 /path/to/newsub2"
}

It 'Should preserve unlisted subsystems when _purge is false' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
Microsoft.DSC:
securityContext: elevated
resources:
- name: preservesubsystem
type: Microsoft.OpenSSH.SSHD/SubsystemList
metadata:
filepath: $script:TestConfigPath
properties:
_purge: false
subsystem:
- name: onlythisone
value: /path/to/this
"@
$out = dsc config set -i "$config_yaml" | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.hadErrors | Should -BeFalse

# Verify all existing subsystems are still present plus the new one
$subsystems = Get-Content $script:TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
$subsystems.Count | Should -Be 3
$subsystems | Should -Contain "subsystem onlythisone /path/to/this"
$subsystems | Should -Contain "subsystem sftp $script:DefaultSftpPath"
$subsystems | Should -Contain "Subsystem test2 /path/to/test2"
}

It 'Should remove unlisted subsystems when _purge is true' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
metadata:
Microsoft.DSC:
securityContext: elevated
resources:
- name: purgesubsystem
type: Microsoft.OpenSSH.SSHD/SubsystemList
metadata:
filepath: $script:TestConfigPath
properties:
_purge: true
subsystem:
- name: sftp
value: $script:AlternatePath
- name: newsub
value: /path/to/newsub
"@
$out = dsc config set -i "$config_yaml" | ConvertFrom-Json -Depth 10
$LASTEXITCODE | Should -Be 0
$out.hadErrors | Should -BeFalse

# Verify only specified subsystems remain
$subsystems = Get-Content $script:TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
$subsystems.Count | Should -Be 2
$sftpLine = $subsystems | Where-Object { $_ -match 'sftp' }
$sftpLine | Should -Match ([regex]::Escape($script:AlternatePath))
$subsystems | Should -Contain "subsystem newsub /path/to/newsub"
$subsystems | Should -Not -Match 'test2'
}
}
}
4 changes: 3 additions & 1 deletion resources/sshdconfig/.project.data.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
],
"CopyFiles": {
"All": [
"sshd_config.dsc.resource.json"
"sshd_config.dsc.resource.json",
"sshd-subsystem.dsc.resource.json",
"sshd-subsystemList.dsc.resource.json"
],
"Windows": [
"sshd-windows.dsc.resource.json"
Expand Down
20 changes: 18 additions & 2 deletions resources/sshdconfig/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ _version = 1

[args]
getInput = "input to get for sshd_config or default shell settings"
exportCompare = "compare exported sshd_config setting to provided input"
exportInput = "input to export from sshd_config"
setInput = "input to set in sshd_config"

[canonical_properties]
inputMustBeBoolean = "value of '%{input}' must be true or false"

[error]
command = "Command"
envVar = "Environment Variable"
Expand Down Expand Up @@ -33,6 +37,8 @@ defaultShellEscapeArgsMustBe0Or1 = "'%{input}' must be a 0 or 1"
defaultShellEscapeArgsMustBeDWord = "escapeArguments must be a DWord"
defaultShellMustBeString = "shell must be a string"
includeWarning = "Include directive found in sshd_config. This resource uses 'sshd -T' to process the overall configuration state, which merges all included files but does not return the Include directive itself"
invalidSetting = "Invalid setting: %{setting}"
matchParsingError = "Error parsing match keyword: %{error}"
traceInput = "Get input:"
windowsOnly = "Microsoft.OpenSSH.SSHD/Windows is only applicable to Windows"

Expand All @@ -57,19 +63,30 @@ missingKeyInCriteria = "missing key in criteria node: '%{input}'"
missingValueInCriteria = "missing value in criteria node: '%{input}'"
missingValueInChildNode = "missing value in child node: '%{input}'"
noArgumentsFound = "no arguments found in node: '%{input}'"
structuredKeywordCannotUseOperator = "structured keyword '%{keyword}' cannot use operator syntax"
structuredKeywordRequiresMinArgs = "structured keyword '%{keyword}' requires at least a name and a value"
valueDebug = "Parsed argument value:"
unknownNode = "unknown node: '%{kind}'"
unknownNodeType = "unknown node type: '%{node}'"

[repeat_keyword]
entryNameRequired = "Entry 'name' field is required and cannot be empty"
entryValueRequired = "Entry '%{name}' requires a 'value' field"
failedToParse = "failed to parse: '%{input}'"
multipleKeywordsNotAllowed = "Multiple keywords not allowed in input (found %{count} keywords)"
nameValueEntryRequiresValue = "Name-value keyword entry requires 'value' field when _exist is true"
noKeywordFoundInInput = "No keyword found in input (expected one keyword like 'subsystem')"

[set]
backingUpConfig = "Backing up existing sshd_config file"
backupCreated = "Backup created at: %{path}"
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
configDoesNotExist = "sshd_config file does not exist, no backup created"
defaultShellDebug = "default_shell: %{shell}"
expectedArrayForKeyword = "Expected array for keyword '%{keyword}'"
failedToParse = "failed to parse: '%{input}'"
failedToParseDefaultShell = "failed to parse input for DefaultShell with error: '%{error}'"
purgeFalseRequiresExistingFile = "_purge=false requires an existing sshd_config file. Use _purge=true to create a new configuration file."
purgeFalseUnsupported = "_purge=false is not supported for keywords that can have multiple values"
settingDefaultShell = "Setting default shell"
settingSshdConfig = "Setting sshd_config"
shellPathDoesNotExist = "shell path does not exist: '%{shell}'"
Expand All @@ -82,7 +99,6 @@ writingTempConfig = "Writing temporary sshd_config file"
[util]
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
getIgnoresInputFilters = "get command does not support filtering based on input settings, provided input will be ignored"
inputMustBeBoolean = "value of '%{input}' must be true or false"
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
sshdElevation = "elevated security context required"
tempFileCreated = "temporary file created at: %{path}"
Expand Down
10 changes: 7 additions & 3 deletions resources/sshdconfig/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ pub enum Command {
/// Export `sshd_config`, eventually to be used for repeatable keywords
Export {
#[clap(short = 'i', long, help = t!("args.exportInput").to_string())]
input: Option<String>
input: Option<String>,
#[clap(short = 'c', long, help = t!("args.exportCompare").to_string())]
compare: bool,
},
Schema {
// Used to inform which schema to generate
Expand All @@ -73,5 +75,7 @@ pub struct DefaultShell {
#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum Setting {
SshdConfig,
WindowsGlobal
}
SshdConfigRepeat,
SshdConfigRepeatList,
WindowsGlobal,
}
Loading