Skip to content
Merged
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
47 changes: 47 additions & 0 deletions Modules/CIPPCore/Public/ConvertTo-CIPPExoHashtable.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function ConvertTo-CIPPExoHashtable {
<#
.SYNOPSIS
Convert a value into the hashtable shape the EXO AdminApi accepts for PswsHashtable parameters.
.DESCRIPTION
Hashtable-typed cmdlet parameters (e.g. Set-Label -AdvancedSettings, New-LabelPolicy -Settings)
sent over the AdminApi REST endpoint deserialize server-side as Newtonsoft JObjects, which the
parameter binder cannot convert to PswsHashtable. Tagging the object with
'@odata.type' = '#Exchange.GenericHashTable' makes the binder accept it - the same convention
used elsewhere in CIPP for MultiValuedProperty add/remove hashtables.

Accepts a dictionary, a PSCustomObject (deserialized JSON object), or an array of key/value
pairs in either the {Key, Value} object shape or the [key, value] pair shape used by label
policy Settings in template JSON.
.PARAMETER InputObject
The value to convert.
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] $InputObject
)

$Result = @{ '@odata.type' = '#Exchange.GenericHashTable' }

if ($InputObject -is [System.Collections.IDictionary]) {
foreach ($Key in @($InputObject.Keys)) {
if ($Key -ne '@odata.type') { $Result[$Key] = $InputObject[$Key] }
}
} elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
foreach ($Entry in $InputObject) {
if ($null -eq $Entry) { continue }
if ($Entry -isnot [string] -and $Entry.PSObject.Properties['Key']) {
$Result[$Entry.Key] = $Entry.Value
} elseif ($Entry -is [System.Collections.IList] -and $Entry.Count -ge 2) {
$Result["$($Entry[0])"] = $Entry[1]
}
}
} else {
foreach ($Prop in $InputObject.PSObject.Properties) {
if ($Prop.Name -ne '@odata.type') { $Result[$Prop.Name] = $Prop.Value }
}
}

return $Result
}
38 changes: 38 additions & 0 deletions Modules/CIPPCore/Public/ConvertTo-CIPPSensitivityLabelParams.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ function ConvertTo-CIPPSensitivityLabelParams {
arrays (which are not valid input in their read form). A flat object (manual JSON authored against
the deploy schema) has no 'LabelActions' and passes through unchanged.

Applied -AdvancedSettings values (e.g. a custom label color) are only readable through the same
read-only 'Settings' array, so before dropping it the writable advanced settings are lifted into
an 'AdvancedSettings' dictionary that New-/Set-Label accept. An explicit 'AdvancedSettings' value
already on the template wins over captured values.

Deploy-time validation/allowlisting still happens in Set-CIPPSensitivityLabel via
Get-CIPPSensitivityLabelField; this function only reshapes.
.PARAMETER Label
Expand Down Expand Up @@ -42,6 +47,39 @@ function ConvertTo-CIPPSensitivityLabelParams {
return [pscustomobject]$Flat
}

# Writable advanced settings that Get-Label only reports inside the read-only Settings array
# ([key, value] pairs). The rest of Settings is system metadata (displayname, contenttype,
# tooltip, ...) that must not be echoed back to New-/Set-Label. Extend this list as more
# -AdvancedSettings keys gain first-class support.
$WritableAdvancedSettings = @('color')
$CapturedAdvanced = @{}
foreach ($Entry in @($Label.Settings)) {
if ($null -eq $Entry) { continue }
$Key = $null
$Value = $null
if ($Entry -isnot [string] -and $Entry.PSObject.Properties['Key']) {
$Key = $Entry.Key
$Value = $Entry.Value
} elseif ("$Entry" -match '^\[\s*(.+?)\s*,\s*(.*?)\s*\]$') {
# Get-Label serializes each entry as the string '[key, value]'
$Key = $Matches[1]
$Value = $Matches[2]
}
if ($Key -and $Key.ToLower() -in $WritableAdvancedSettings -and -not [string]::IsNullOrWhiteSpace("$Value")) {
$CapturedAdvanced[$Key.ToLower()] = "$Value"
}
}
if ($CapturedAdvanced.Count -gt 0) {
# Explicit AdvancedSettings on the template win over values captured from Settings.
$Explicit = $Flat['AdvancedSettings']
if ($Explicit -is [System.Collections.IDictionary]) {
foreach ($ExplicitKey in @($Explicit.Keys)) { $CapturedAdvanced[$ExplicitKey] = $Explicit[$ExplicitKey] }
} elseif ($null -ne $Explicit) {
foreach ($ExplicitProp in $Explicit.PSObject.Properties) { $CapturedAdvanced[$ExplicitProp.Name] = $ExplicitProp.Value }
}
$Flat['AdvancedSettings'] = $CapturedAdvanced
}

foreach ($Raw in @($Label.LabelActions)) {
if ($null -eq $Raw) { continue }
$Action = if ($Raw -is [string]) { $Raw | ConvertFrom-Json } else { $Raw }
Expand Down
22 changes: 22 additions & 0 deletions Modules/CIPPCore/Public/Set-CIPPSensitivityLabel.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ function Set-CIPPSensitivityLabel {
$PolicySource = $Template.PolicyParams
$LabelName = $LabelParams.Name

# PswsHashtable parameters need the Exchange.GenericHashTable odata type to bind over the AdminApi.
if ($LabelParams.ContainsKey('AdvancedSettings')) {
$LabelParams['AdvancedSettings'] = ConvertTo-CIPPExoHashtable -InputObject $LabelParams['AdvancedSettings']
}

# Priority is valid on Set-Label but not New-Label, so it is applied via a dedicated Set-Label call below.
$LabelPriority = $null
if ($LabelParams.ContainsKey('Priority')) {
Expand All @@ -44,6 +49,16 @@ function Set-CIPPSensitivityLabel {
}

try {
# A custom label color travels as the 'color' advanced setting. Validate the hex format up front
# so a bad value fails with a clear message instead of an opaque compliance-endpoint error.
# An empty string is valid: it clears a previously set color.
if ($LabelParams.ContainsKey('AdvancedSettings')) {
$ColorValue = $LabelParams['AdvancedSettings']['color']
if (-not [string]::IsNullOrEmpty("$ColorValue") -and "$ColorValue" -notmatch '^#[0-9A-Fa-f]{6}$') {
throw "Invalid label color '$ColorValue' in the AdvancedSettings of '$LabelName'. Use a 6-digit hex color like #40E0D0."
}
}

$ExistingLabels = try { New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Label' -Compliance | Select-Object Name, DisplayName } catch { @() }
$ExistingLabelPolicies = try { New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-LabelPolicy' -Compliance | Select-Object Name } catch { @() }

Expand Down Expand Up @@ -71,6 +86,13 @@ function Set-CIPPSensitivityLabel {

if ($PolicySource) {
$PolicyHash = Format-CIPPCompliancePolicyParams -Source $PolicySource -AllowedFields $PolicyAllowedFields
# Settings/AdvancedSettings are PswsHashtable on New-/Set-LabelPolicy; template JSON authors
# Settings as [key, value] pairs, which the helper also normalizes.
foreach ($HashtableParam in @('AdvancedSettings', 'Settings')) {
if ($PolicyHash.ContainsKey($HashtableParam)) {
$PolicyHash[$HashtableParam] = ConvertTo-CIPPExoHashtable -InputObject $PolicyHash[$HashtableParam]
}
}
if (-not $PolicyHash.ContainsKey('Labels') -or -not $PolicyHash['Labels']) {
$PolicyHash['Labels'] = @($LabelName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ Function Invoke-EditSensitivityLabel {
}
}

# PswsHashtable parameters need the Exchange.GenericHashTable odata type to bind over the AdminApi.
foreach ($HashtableParam in @('AdvancedSettings', 'Settings')) {
if ($Params.ContainsKey($HashtableParam)) {
$Params[$HashtableParam] = ConvertTo-CIPPExoHashtable -InputObject $Params[$HashtableParam]
}
}

$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Label' -cmdParams $Params -Compliance -useSystemMailbox $true
$Result = "Updated sensitivity label $Identity"
Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ Function Invoke-ListSensitivityLabel {
$labelGuid = $_.Guid
@($Policies | Where-Object { $_.Labels -contains $labelGuid -or $_.Labels -contains $_.ImmutableId }) | Select-Object -ExpandProperty Name
}
},
@{l = 'Color'; e = {
# The 'color' advanced setting is only exposed inside the read-only Settings array,
# either as a {Key, Value} object or as the serialized string '[color, #RRGGBB]'.
foreach ($Entry in @($_.Settings)) {
if ($null -eq $Entry) { continue }
if ($Entry -isnot [string] -and $Entry.PSObject.Properties['Key']) {
if ("$($Entry.Key)" -eq 'color') { "$($Entry.Value)"; break }
} elseif ("$Entry" -match '^\[\s*color\s*,\s*(.*?)\s*\]$') {
$Matches[1]; break
}
}
}
}

$StatusCode = [HttpStatusCode]::OK
Expand Down