@@ -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