Skip to content

Commit b66b178

Browse files
Merge pull request KelvinTegelaar#1889 from Brad-M-K/pass-config
Password Configuration Management & Generation
2 parents b1bb585 + fee2acc commit b66b178

3 files changed

Lines changed: 663 additions & 25 deletions

File tree

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecPasswordConfig.ps1

Lines changed: 245 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,267 @@ Function Invoke-ExecPasswordConfig {
77
#>
88
[CmdletBinding()]
99
param($Request, $TriggerMetadata)
10+
11+
$APIName = $Request.Params.CIPPEndpoint
12+
$StatusCode = [HttpStatusCode]::OK
1013
$Table = Get-CIPPTable -TableName Settings
11-
$PasswordType = (Get-CIPPAzDataTableEntity @Table)
14+
$PasswordSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'settings' and RowKey eq 'settings'"
15+
16+
# Helper functions for consistent data conversion
17+
function ConvertTo-BoolString ($raw) {
18+
if ($null -eq $raw) { return $false }
19+
$stringValue = "$raw"
20+
return ($stringValue -eq 'true' -or $stringValue -eq '1' -or $stringValue -eq 'yes')
21+
}
22+
23+
function ConvertTo-Bool ($raw) {
24+
if ($null -eq $raw) { return $false }
25+
$stringValue = "$raw"
26+
return ($stringValue -eq 'true' -or $stringValue -eq '1' -or $stringValue -eq 'yes')
27+
}
28+
29+
function Test-RequestBody ($body) {
30+
if (-not $body) { return $false }
31+
if ($body -is [string] -or $body -is [array]) { return $false }
32+
return $true
33+
}
1234

1335

1436
$results = try {
1537
if ($Request.Query.List) {
16-
@{ passwordType = $PasswordType.passwordType }
38+
if (-not $PasswordSettings) {
39+
# Return default values if not set
40+
@{
41+
passwordType = 'Classic'
42+
charCount = 14
43+
includeUppercase = $true
44+
includeLowercase = $true
45+
includeDigits = $true
46+
includeSpecialChars = $true
47+
specialCharSet = '$%&*#'
48+
wordCount = 4
49+
separator = '-'
50+
capitalizeWords = $false
51+
appendNumber = $false
52+
appendSpecialChar = $false
53+
}
54+
} else {
55+
# Migrate legacy 'Correct-Battery-Horse' type to 'Passphrase'
56+
$storedType = if ($PasswordSettings.passwordType) { $PasswordSettings.passwordType } else { 'Classic' }
57+
$needsMigration = $storedType -eq 'Correct-Battery-Horse'
58+
if ($needsMigration) {
59+
$storedType = 'Passphrase'
60+
}
61+
62+
$resolvedConfig = @{
63+
passwordType = $storedType
64+
charCount = if ($PasswordSettings.charCount -and [int]::TryParse("$($PasswordSettings.charCount)", [ref]$null)) { [int]$PasswordSettings.charCount } else { 14 }
65+
includeUppercase = if ($null -ne $PasswordSettings.includeUppercase) { ConvertTo-Bool $PasswordSettings.includeUppercase } else { $true }
66+
includeLowercase = if ($null -ne $PasswordSettings.includeLowercase) { ConvertTo-Bool $PasswordSettings.includeLowercase } else { $true }
67+
includeDigits = if ($null -ne $PasswordSettings.includeDigits) { ConvertTo-Bool $PasswordSettings.includeDigits } else { $true }
68+
includeSpecialChars = if ($null -ne $PasswordSettings.includeSpecialChars) { ConvertTo-Bool $PasswordSettings.includeSpecialChars } else { $true }
69+
specialCharSet = if ($PasswordSettings.specialCharSet) { $PasswordSettings.specialCharSet } else { '$%&*#' }
70+
wordCount = if ($PasswordSettings.wordCount -and [int]::TryParse("$($PasswordSettings.wordCount)", [ref]$null)) { [int]$PasswordSettings.wordCount } else { 4 }
71+
separator = if ($null -ne $PasswordSettings.separator) { $PasswordSettings.separator } else { '-' }
72+
capitalizeWords = if ($null -ne $PasswordSettings.capitalizeWords) { ConvertTo-Bool $PasswordSettings.capitalizeWords } else { $false }
73+
appendNumber = if ($null -ne $PasswordSettings.appendNumber) { ConvertTo-Bool $PasswordSettings.appendNumber } else { $false }
74+
appendSpecialChar = if ($null -ne $PasswordSettings.appendSpecialChar) { ConvertTo-Bool $PasswordSettings.appendSpecialChar } else { $false }
75+
}
76+
77+
# Persist migrated config so legacy type is upgraded in storage
78+
if ($needsMigration) {
79+
$MigratedEntity = @{
80+
'PartitionKey' = 'settings'
81+
'RowKey' = 'settings'
82+
'passwordType' = $resolvedConfig.passwordType
83+
'charCount' = "$($resolvedConfig.charCount)"
84+
'includeUppercase' = $resolvedConfig.includeUppercase
85+
'includeLowercase' = $resolvedConfig.includeLowercase
86+
'includeDigits' = $resolvedConfig.includeDigits
87+
'includeSpecialChars' = $resolvedConfig.includeSpecialChars
88+
'specialCharSet' = $resolvedConfig.specialCharSet
89+
'wordCount' = "$($resolvedConfig.wordCount)"
90+
'separator' = $resolvedConfig.separator
91+
'capitalizeWords' = $resolvedConfig.capitalizeWords
92+
'appendNumber' = $resolvedConfig.appendNumber
93+
'appendSpecialChar' = $resolvedConfig.appendSpecialChar
94+
}
95+
Add-CIPPAzDataTableEntity @Table -Entity $MigratedEntity -Force | Out-Null
96+
Write-LogMessage -headers $Request.Headers -API $APIName -message "Migrated legacy password type 'Correct-Battery-Horse' to 'Passphrase'" -Sev 'Info'
97+
}
98+
99+
$resolvedConfig
100+
}
17101
} else {
102+
# ── Validate request body ────────────────────────────────────────
103+
if (-not (Test-RequestBody $Request.Body)) {
104+
$StatusCode = [HttpStatusCode]::BadRequest
105+
throw 'Request body must be a valid JSON object'
106+
}
107+
108+
# Password type validation
109+
$pwType = if ($null -ne $Request.Body.passwordType) { "$($Request.Body.passwordType)" } else { '' }
110+
# Accept legacy type name and normalize to new name
111+
if ($pwType -eq 'Correct-Battery-Horse') {
112+
$pwType = 'Passphrase'
113+
}
114+
if ($pwType -notin @('Classic', 'Passphrase')) {
115+
$StatusCode = [HttpStatusCode]::BadRequest
116+
throw 'Please select a valid password type (Classic or Passphrase)'
117+
}
118+
119+
$includeUppercase = ConvertTo-Bool $Request.Body.includeUppercase
120+
$includeLowercase = ConvertTo-Bool $Request.Body.includeLowercase
121+
$includeDigits = ConvertTo-Bool $Request.Body.includeDigits
122+
$includeSpecialChars = ConvertTo-Bool $Request.Body.includeSpecialChars
123+
$capitalizeWords = ConvertTo-Bool $Request.Body.capitalizeWords
124+
$appendNumber = ConvertTo-Bool $Request.Body.appendNumber
125+
$appendSpecialChar = ConvertTo-Bool $Request.Body.appendSpecialChar
126+
127+
# Char count validation (classic only)
128+
$charCount = 0
129+
if ($pwType -eq 'Classic') {
130+
if (-not [int]::TryParse("$($Request.Body.charCount)", [ref]$charCount)) {
131+
$StatusCode = [HttpStatusCode]::BadRequest
132+
throw 'Password length must be a valid number'
133+
} elseif ($charCount -lt 8 -or $charCount -gt 256) {
134+
$StatusCode = [HttpStatusCode]::BadRequest
135+
throw 'Password length must be between 8 and 256 characters'
136+
}
137+
} else {
138+
# Still parse for storage, but don't reject invalid values for the inactive mode
139+
if ([int]::TryParse("$($Request.Body.charCount)", [ref]$charCount)) { } else { $charCount = 14 }
140+
}
141+
142+
# Word count validation (passphrase only)
143+
$wordCount = 0
144+
if ($pwType -eq 'Passphrase') {
145+
if (-not [int]::TryParse("$($Request.Body.wordCount)", [ref]$wordCount)) {
146+
$StatusCode = [HttpStatusCode]::BadRequest
147+
throw 'Word count must be a valid number'
148+
} elseif ($wordCount -lt 2 -or $wordCount -gt 10) {
149+
$StatusCode = [HttpStatusCode]::BadRequest
150+
throw 'Word count must be between 2 and 10 words'
151+
}
152+
} else {
153+
if ([int]::TryParse("$($Request.Body.wordCount)", [ref]$wordCount)) { } else { $wordCount = 4 }
154+
}
155+
156+
# Special character set validation with enhanced security
157+
$specialCharSet = if ($null -ne $Request.Body.specialCharSet) { "$($Request.Body.specialCharSet)" } else { '' }
158+
# Define safe and easily typable special character set including forward slash
159+
$allowedSpecialPattern = '^[!@#$%^&*()\-_=+/]+$'
160+
if ($includeSpecialChars -or $appendSpecialChar) {
161+
if ([string]::IsNullOrEmpty($specialCharSet)) {
162+
$StatusCode = [HttpStatusCode]::BadRequest
163+
throw 'Special characters cannot be empty when enabled'
164+
} elseif ($specialCharSet.Length -gt 32) {
165+
$StatusCode = [HttpStatusCode]::BadRequest
166+
throw 'Special characters set must be 32 characters or fewer'
167+
} elseif ($specialCharSet -match '[\x00-\x1F\x7F]') {
168+
$StatusCode = [HttpStatusCode]::BadRequest
169+
throw 'Special characters cannot contain control characters'
170+
} elseif ($specialCharSet -notmatch $allowedSpecialPattern) {
171+
$StatusCode = [HttpStatusCode]::BadRequest
172+
throw 'Special characters contain invalid symbols. Only safe typable characters allowed: !@#$%^&*()-_=+/'
173+
}
174+
}
175+
176+
# Separator validation with enhanced security - allow space or empty
177+
$separator = if ($null -ne $Request.Body.separator) { "$($Request.Body.separator)" } else { '' }
178+
if ($separator.Length -gt 5) {
179+
$StatusCode = [HttpStatusCode]::BadRequest
180+
throw 'Separator must be 5 characters or fewer'
181+
}
182+
# Allow empty separator or single space, otherwise validate against safe characters
183+
if ($separator -ne '' -and $separator -ne ' ') {
184+
# Use the same validation pattern as special characters for consistency
185+
if ($separator -match '[\x00-\x1F\x7F]') {
186+
$StatusCode = [HttpStatusCode]::BadRequest
187+
throw 'Separator cannot contain control characters'
188+
}
189+
if ($separator -match '[\u2000-\u200F\u2028-\u202F\u205F\u3000]') {
190+
$StatusCode = [HttpStatusCode]::BadRequest
191+
throw 'Separator cannot contain Unicode whitespace characters'
192+
}
193+
if ($separator -notmatch $allowedSpecialPattern) {
194+
$StatusCode = [HttpStatusCode]::BadRequest
195+
throw 'Separator contains invalid symbols. Only safe typable characters allowed: !@#$%^&*()-_=+/ (or space/empty)'
196+
}
197+
}
198+
199+
# Microsoft 365 complexity validation: at least 3 of 4 character types
200+
if ($pwType -eq 'Classic') {
201+
$enabledCount = 0
202+
if ($includeUppercase) { $enabledCount++ }
203+
if ($includeLowercase) { $enabledCount++ }
204+
if ($includeDigits) { $enabledCount++ }
205+
if ($includeSpecialChars) { $enabledCount++ }
206+
if ($enabledCount -lt 3) {
207+
$StatusCode = [HttpStatusCode]::BadRequest
208+
throw 'Classic passwords must include at least 3 of these 4 types: uppercase letters, lowercase letters, numbers, and special characters'
209+
}
210+
} else {
211+
# Passphrase complexity validation
212+
$hasLower = $true # words always contain lowercase
213+
$hasUpper = $capitalizeWords
214+
$hasDigits = $appendNumber
215+
$hasSpecial = $appendSpecialChar
216+
217+
# Check if separator contains special characters or digits - validate actual content
218+
if ($separator) {
219+
$HasSpecialSeparator = $separator -match '[!@#$%^&*()_+\-=[\]{};:,.<>/?|~]'
220+
$HasDigitSeparator = $separator -match '\d'
221+
if ($HasSpecialSeparator) {
222+
$hasSpecial = $true
223+
}
224+
if ($HasDigitSeparator) {
225+
$hasDigits = $true
226+
}
227+
}
228+
229+
$ppTypes = @($hasLower, $hasUpper, $hasDigits, $hasSpecial).Where({ $_ }).Count
230+
if ($ppTypes -lt 3) {
231+
$StatusCode = [HttpStatusCode]::BadRequest
232+
throw 'Passphrases must include at least 3 of these 4 types: lowercase letters (from words), uppercase letters (capitalization), numbers (appended), and special characters (appended)'
233+
}
234+
}
235+
236+
# ── Persist validated config ──────────────────────────────────────
18237
$PasswordConfig = @{
19-
'passwordType' = "$($Request.Body.passwordType)"
20-
'passwordCount' = '12'
21-
'PartitionKey' = 'settings'
22-
'RowKey' = 'settings'
238+
'PartitionKey' = 'settings'
239+
'RowKey' = 'settings'
240+
'passwordType' = $pwType
241+
'charCount' = "$charCount"
242+
'includeUppercase' = $includeUppercase
243+
'includeLowercase' = $includeLowercase
244+
'includeDigits' = $includeDigits
245+
'includeSpecialChars' = $includeSpecialChars
246+
'specialCharSet' = $specialCharSet
247+
'wordCount' = "$wordCount"
248+
'separator' = $separator
249+
'capitalizeWords' = $capitalizeWords
250+
'appendNumber' = $appendNumber
251+
'appendSpecialChar' = $appendSpecialChar
23252
}
24253

25254
Add-CIPPAzDataTableEntity @Table -Entity $PasswordConfig -Force | Out-Null
26-
'Successfully set the configuration'
255+
Write-LogMessage -headers $Request.Headers -API $APIName -message "Successfully set password configuration" -Sev 'Info'
256+
"Successfully set the configuration"
27257
}
28258
} catch {
29-
"Failed to set configuration: $($_.Exception.message)"
259+
if ($StatusCode -eq [HttpStatusCode]::OK) {
260+
$StatusCode = [HttpStatusCode]::InternalServerError
261+
}
262+
$ErrorMessage = Get-CippException -Exception $_
263+
Write-LogMessage -headers $Request.Headers -API $APIName -message "Failed to set password configuration: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
264+
"Failed to set configuration: $($ErrorMessage.NormalizedError)"
30265
}
31266

32-
33-
$body = [pscustomobject]@{'Results' = $Results }
267+
$body = [pscustomobject]@{'Results' = if ($null -ne $results) { $results } else { "Operation completed" } }
34268

35269
return ([HttpResponseContext]@{
36-
StatusCode = [HttpStatusCode]::OK
270+
StatusCode = $StatusCode
37271
Body = $body
38272
})
39273

0 commit comments

Comments
 (0)