From 757d743a4dc0165ea8841f2d679e9de29ea0d04f Mon Sep 17 00:00:00 2001 From: Mark Spreeuwenberg Date: Fri, 12 Jun 2026 14:56:06 +0200 Subject: [PATCH 1/9] Added addtional example --- permissions/permissions.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/permissions/permissions.ps1 b/permissions/permissions.ps1 index dfbbeb6..2b43f1c 100644 --- a/permissions/permissions.ps1 +++ b/permissions/permissions.ps1 @@ -16,6 +16,9 @@ try { @{ RoleName = 'Leidinggevende' }, + @{ + RoleName = 'Consignatiedienst Cluster A' + }, @{ RoleName = 'ADMIN' } From 5cd7f1baac6481a97811624e30cd69d48ea2b5f7 Mon Sep 17 00:00:00 2001 From: Mark Spreeuwenberg Date: Fri, 12 Jun 2026 14:56:38 +0200 Subject: [PATCH 2/9] multiple minor fixes and allow - in permission name --- permissions/subPermissions.ps1 | 334 +++++++++++++++++++++++++++++++-- 1 file changed, 320 insertions(+), 14 deletions(-) diff --git a/permissions/subPermissions.ps1 b/permissions/subPermissions.ps1 index a893f3a..22884dc 100644 --- a/permissions/subPermissions.ps1 +++ b/permissions/subPermissions.ps1 @@ -7,24 +7,32 @@ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 # Script Mapping lookup values and permission mapping -$permissionMapping = @( - @{ +$permissionMapping = @{ + 'Planner' = @{ role = 'Planner' resourceGroup = 'Planner {{LocationOwn}}' exchangeGroup = 'Company' shiftGroup = 'Company' worklocationGroup = 'Root' userGroup = 'Root' - }, - @{ + } + 'Leidinggevende' = @{ role = 'Leidinggevende' resourceGroup = '{{CostCenterOwn}}' exchangeGroup = 'Company' shiftGroup = 'Company' worklocationGroup = 'Root' userGroup = 'Root' - }, - @{ + } + 'Consignatiedienst Cluster A' = @{ + role = 'Consignatiedienst' + resourceGroup = 'Cluster A' + exchangeGroup = 'Company' + shiftGroup = 'Company' + worklocationGroup = 'Root' + userGroup = 'Root' + } + 'ADMMIN' = @{ role = 'ADMIN' resourceGroup = 'ADMIN' exchangeGroup = 'ADMIN' @@ -32,7 +40,7 @@ $permissionMapping = @( worklocationGroup = 'Root' userGroup = 'Root' } -) +} # Lookup values which are used in the mapping to determine {{REPLACEMENT}} $lookupValues = @{ @@ -145,7 +153,7 @@ try { } if ($actionContext.Operation -ne 'revoke' ) { - $subPermission = $permissionMapping | Where-Object { $_.role -eq $actionContext.References.Permission.Reference } + $subPermission = $permissionMapping[$actionContext.References.Permission.Reference] if ($null -eq $subPermission) { throw "Permission [$($actionContext.References.Permission.Reference)] does not have a valid script mapping defined" } @@ -179,6 +187,8 @@ try { } } + #Write-Warning ($correlatedAccount | ConvertTo-Json) + if ($null -ne $correlatedAccount) { $lifecycleProcess = 'ManageSubPermissions' $currentRoles = [System.Collections.Generic.List[object]]::new() @@ -214,7 +224,7 @@ try { $mappedProperty = ($contract | Select-Object $lookupValue).$lookupValue $null = Resolve-ReplaceHolderValue -ReplaceVariable $replaceVariable.Key -MappedProperty $mappedProperty -Contract $contract -DesiredPermission $desiredPermission } - $desiredPermissionUniqueKey = "$($actionContext.References.Permission.Reference)-$($desiredPermission.ResourceGroup)" + $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission } } @@ -223,7 +233,7 @@ try { # Processing Static permissions body without placeholder(s) else { $desiredPermission = $subPermission.PSObject.Copy() - $desiredPermissionUniqueKey = "$($actionContext.References.Permission.Reference)-$($desiredPermission.ResourceGroup)" + $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission } @@ -239,13 +249,309 @@ try { if ($actionContext.DryRun -eq $true) { Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" } + + Write-Warning ($currentRoles | ConvertTo-Json) $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } if (-not $existingRole) { + Write-Warning "not exist" $null = $currentRoles.Add($permission.value) } elseif ($existingRole.count -eq 1) { - $currentRoles.Remove($existingRole) - $currentRoles.Add($permission.value) + Write-Warning "exist" + $null = $currentRoles.Remove($existingRole) + $null = $currentRoles.Add($permission.value) + } + + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Action = 'GrantPermission' + Message = "Granted access to permission $($permission.Name)" + IsError = $false + }) + } + } + } + + # Process and calculate current permissions Revoke + foreach ($permission in $currentPermissions.GetEnumerator()) { + $roleName = $permission.Name -split '&&' | Select-Object -First 1 + $resourceGroup = $permission.Name -split '&&' | Select-Object -Last 1 + if (-not $desiredPermissions.ContainsKey($permission.Name)) { + if ($actionContext.DryRun -eq $true) { + Write-Information "[DryRun] Revoke access to permission $($permission.Name), will be executed during enforcement" + } + $existingRole = $currentRoles | Where-Object { $_.role -eq $roleName -and $_.resourceGroup -eq $resourceGroup } + + # Remove from current roles for later update + $null = $currentRoles.Remove($existingRole) + + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Action = 'RevokePermission' + Message = "Revoked access to permission $($permission.Name)" + IsError = $false + }) + } + } + + # Actual update InPlanning account with desired roles + $correlatedAccount.roles = @($currentRoles) + $body = ($correlatedAccount | ConvertTo-Json -Depth 10) + $splatUpdateUserParams = @{ + Uri = "$($actionContext.Configuration.BaseUrl)/api/users" + Headers = $headers + Method = 'PUT' + Body = $body + ContentType = 'application/json;charset=utf-8' + } + if (-not($actionContext.DryRun -eq $true)) { + $null = Invoke-RestMethod @splatUpdateUserParams + } + + $outputContext.Success = $true + break + } + + 'NotFound' { + Write-Information "InPlanning account: [$($actionContext.References.Account)] could not be found, indicating that it may have been deleted" + $outputContext.Success = $false + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Message = "InPlanning account: [$($actionContext.References.Account)] could not be found, indicating that it may have been deleted" + IsError = $true + }) + break + } + } +} +catch { + if ($outputContext.AuditLogs.Count -gt 0) { + $null = $outputContext.AuditLogs = [System.Collections.Generic.List[object]]::new() + } + + $outputContext.Success = $false + $ex = $PSItem + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-InPlanningError -ErrorObject $ex + $auditMessage = "Could not manage InPlanning permissions. Error: $($errorObj.FriendlyMessage)" + Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Could not manage InPlanning permissions. Error: $($_.Exception.Message)" + Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" + } + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Message = $auditMessage + IsError = $true + }) +}#region functions +function Resolve-InPlanningError { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [object] + $ErrorObject + ) + process { + $httpErrorObj = [PSCustomObject]@{ + ScriptLineNumber = $ErrorObject.InvocationInfo.ScriptLineNumber + Line = $ErrorObject.InvocationInfo.Line + ErrorDetails = $ErrorObject.Exception.Message + FriendlyMessage = $ErrorObject.Exception.Message + } + if (-not [string]::IsNullOrEmpty($ErrorObject.ErrorDetails.Message)) { + $httpErrorObj.ErrorDetails = $ErrorObject.ErrorDetails.Message + } + elseif ($ErrorObject.Exception.GetType().FullName -eq 'System.Net.WebException') { + if ($null -ne $ErrorObject.Exception.Response) { + $streamReaderResponse = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd() + if (-not [string]::IsNullOrEmpty($streamReaderResponse)) { + $httpErrorObj.ErrorDetails = $streamReaderResponse + } + } + } + try { + $errorDetailsObject = ($httpErrorObj.ErrorDetails | ConvertFrom-Json) + if ($errorDetailsObject.error_description) { + $httpErrorObj.FriendlyMessage = $errorDetailsObject.error_description + } + else { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + } + } + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + Write-Warning $_.Exception.Message + } + Write-Output $httpErrorObj + } +} + +function Get-AccessToken { + [CmdletBinding()] + param ( + ) + process { + try { + $tokenHeaders = [System.Collections.Generic.Dictionary[string, string]]::new() + $tokenHeaders.Add('Content-Type', 'application/x-www-form-urlencoded') + + $splatGetTokenParams = @{ + Uri = "$($actionContext.Configuration.BaseUrl)/api/token" + Headers = $tokenHeaders + Method = 'POST' + Body = @{ + client_id = $actionContext.Configuration.clientId + client_secret = $actionContext.Configuration.clientSecret + grant_type = 'client_credentials' + } + } + Write-Output (Invoke-RestMethod @splatGetTokenParams).access_token + } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} + +function Resolve-ReplaceHolderValue { + param ( + [string] + $replaceVariable, + + [string] + $mappedProperty, + + $desiredPermission, + + $contract + ) + # Replace replace placeholder with actual value + if (-not [string]::IsNullOrEmpty($mappedProperty)) { + $keys = @($desiredPermission.Keys) + for ($i = 0; $i -lt $keys.Count; $i++) { + if ($desiredPermission[$keys[$i]] -like "*$($replaceVariable)*") { + $desiredPermission[$keys[$i]] = $desiredPermission[$keys[$i]] -replace ($replaceVariable, $mappedProperty) + } + } + } + else { + throw "Permission expects [$($replaceVariable)] to grant the permission but the specified value is empty for contract with id: [$($contract.ExternalId)]" + } +} +#endregion + +# Begin +try { + # Verify if [AccountReference] has a value + if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { + throw 'The account reference could not be found' + } + + if ($actionContext.Operation -ne 'revoke' ) { + $subPermission = $permissionMapping[$actionContext.References.Permission.Reference] + if ($null -eq $subPermission) { + throw "Permission [$($actionContext.References.Permission.Reference)] does not have a valid script mapping defined" + } + + $lookupValuesToCheck = $subPermission.Values -match '\{\{[^}]+\}\}' + foreach ($replaceVariable in $lookupValuesToCheck) { + if ($replaceVariable -notin $lookupValues.keys ) { + throw "Permission [$($actionContext.References.Permission.Reference)] expects a value for [$($replaceVariable)], but it was not provided as script lookup value" + } + } + } + + # Set Headers for API calls + $accessToken = Get-AccessToken + $headers = [System.Collections.Generic.Dictionary[string, string]]::new() + $headers.Add('Content-Type', 'application/json') + $headers.Add('Authorization', 'Bearer ' + $accessToken) + + Write-Information 'Verifying if a InPlanning account exists' + try { + $splatGetUserParams = @{ + Uri = "$($actionContext.Configuration.BaseUrl)/api/users/$($actionContext.References.Account)" + Headers = $headers + Method = 'GET' + } + $correlatedAccount = Invoke-RestMethod @splatGetUserParams + } + catch { + if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { + $correlatedAccount = $null + } + } + + if ($null -ne $correlatedAccount) { + $lifecycleProcess = 'ManageSubPermissions' + $currentRoles = [System.Collections.Generic.List[object]]::new() + if ($null -ne $correlatedAccount.roles) { + [System.Collections.Generic.List[object]]$currentRoles = $correlatedAccount.roles.PSObject.copy() + } + } + else { + $lifecycleProcess = 'NotFound' + } + + switch ($lifecycleProcess) { + 'ManageSubPermissions' { + # Collect current permissions + $currentPermissions = @{} + foreach ($permission in $actionContext.CurrentPermissions) { + $currentPermissions[$permission.Reference.Id] = $subPermission + } + + # Collect and calculate desired permissions + $desiredPermissions = @{} + if (-not($actionContext.Operation -eq 'revoke')) { + + # Processing Dynamic permissions body with placeholder(s) + if ($subPermission.Values -match '\{\{[^}]+\}\}') { + Write-Information "Permission [$($actionContext.References.Permission.Reference)] contains placeholder values which need to be resolved" + foreach ($contract in $personContext.Person.Contracts) { + if ($contract.Context.InConditions -or ($actionContext.DryRun -eq $true)) { + $desiredPermission = $subPermission.PSObject.Copy() + foreach ($replaceVariable in $lookupValues.GetEnumerator()) { + # Perform lookup in HelloId contract for the correct + $lookupValue = $replaceVariable.Value + $mappedProperty = ($contract | Select-Object $lookupValue).$lookupValue + $null = Resolve-ReplaceHolderValue -ReplaceVariable $replaceVariable.Key -MappedProperty $mappedProperty -Contract $contract -DesiredPermission $desiredPermission + } + $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" + $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission + } + } + } + + # Processing Static permissions body without placeholder(s) + else { + $desiredPermission = $subPermission.PSObject.Copy() + $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" + $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission + } + + # Process desired permissions calculation Grant and Update + foreach ($permission in $desiredPermissions.GetEnumerator()) { + $outputContext.SubPermissions.Add([PSCustomObject]@{ + DisplayName = $permission.Name + Reference = [PSCustomObject]@{ + Id = $permission.Name + } + }) + if (-not $currentPermissions.ContainsKey($permission.Name)) { + if ($actionContext.DryRun -eq $true) { + Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" + } + + Write-Warning ($currentRoles | ConvertTo-Json) + $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } + if (-not $existingRole) { + Write-Warning "not exist" + $null = $currentRoles.Add($permission.value) + } + elseif ($existingRole.count -eq 1) { + Write-Warning "exist" + $null = $currentRoles.Remove($existingRole) + $null = $currentRoles.Add($permission.value) } $outputContext.AuditLogs.Add([PSCustomObject]@{ @@ -259,8 +565,8 @@ try { # Process and calculate current permissions Revoke foreach ($permission in $currentPermissions.GetEnumerator()) { - $roleName = $permission.Name -split '-' | Select-Object -First 1 - $resourceGroup = $permission.Name -split '-' | Select-Object -Last 1 + $roleName = $permission.Name -split '&&' | Select-Object -First 1 + $resourceGroup = $permission.Name -split '&&' | Select-Object -Last 1 if (-not $desiredPermissions.ContainsKey($permission.Name)) { if ($actionContext.DryRun -eq $true) { Write-Information "[DryRun] Revoke access to permission $($permission.Name), will be executed during enforcement" From 28b3053b7f1693aec977b2715204cfc7f73b604c Mon Sep 17 00:00:00 2001 From: Mark Spreeuwenberg Date: Fri, 12 Jun 2026 14:59:12 +0200 Subject: [PATCH 3/9] remove log message --- permissions/subPermissions.ps1 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/permissions/subPermissions.ps1 b/permissions/subPermissions.ps1 index 22884dc..2deec7f 100644 --- a/permissions/subPermissions.ps1 +++ b/permissions/subPermissions.ps1 @@ -187,8 +187,6 @@ try { } } - #Write-Warning ($correlatedAccount | ConvertTo-Json) - if ($null -ne $correlatedAccount) { $lifecycleProcess = 'ManageSubPermissions' $currentRoles = [System.Collections.Generic.List[object]]::new() @@ -250,14 +248,11 @@ try { Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" } - Write-Warning ($currentRoles | ConvertTo-Json) $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } if (-not $existingRole) { - Write-Warning "not exist" $null = $currentRoles.Add($permission.value) } elseif ($existingRole.count -eq 1) { - Write-Warning "exist" $null = $currentRoles.Remove($existingRole) $null = $currentRoles.Add($permission.value) } @@ -542,14 +537,11 @@ try { Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" } - Write-Warning ($currentRoles | ConvertTo-Json) $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } if (-not $existingRole) { - Write-Warning "not exist" $null = $currentRoles.Add($permission.value) } elseif ($existingRole.count -eq 1) { - Write-Warning "exist" $null = $currentRoles.Remove($existingRole) $null = $currentRoles.Add($permission.value) } From 5a311aed3f29615861b44f05e6da577c0943054f Mon Sep 17 00:00:00 2001 From: Mark Spreeuwenberg Date: Fri, 12 Jun 2026 15:06:35 +0200 Subject: [PATCH 4/9] updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b064181..045aa91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [3.0.2] - 12-06-2026 + +### Changed +- Support '-' in permission names +- Made permission name and role name independant +- Minor fixes + ## [3.0.1] - 01-05-2026 ### Added From 04a717b2f2fbbf5cc86e89f0e164819b4e47f1f7 Mon Sep 17 00:00:00 2001 From: Mark Spreeuwenberg Date: Fri, 12 Jun 2026 15:12:07 +0200 Subject: [PATCH 5/9] removed duplicate code --- permissions/subPermissions.ps1 | 291 +-------------------------------- 1 file changed, 1 insertion(+), 290 deletions(-) diff --git a/permissions/subPermissions.ps1 b/permissions/subPermissions.ps1 index 2deec7f..bc3908d 100644 --- a/permissions/subPermissions.ps1 +++ b/permissions/subPermissions.ps1 @@ -247,296 +247,7 @@ try { if ($actionContext.DryRun -eq $true) { Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" } - - $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } - if (-not $existingRole) { - $null = $currentRoles.Add($permission.value) - } - elseif ($existingRole.count -eq 1) { - $null = $currentRoles.Remove($existingRole) - $null = $currentRoles.Add($permission.value) - } - - $outputContext.AuditLogs.Add([PSCustomObject]@{ - Action = 'GrantPermission' - Message = "Granted access to permission $($permission.Name)" - IsError = $false - }) - } - } - } - - # Process and calculate current permissions Revoke - foreach ($permission in $currentPermissions.GetEnumerator()) { - $roleName = $permission.Name -split '&&' | Select-Object -First 1 - $resourceGroup = $permission.Name -split '&&' | Select-Object -Last 1 - if (-not $desiredPermissions.ContainsKey($permission.Name)) { - if ($actionContext.DryRun -eq $true) { - Write-Information "[DryRun] Revoke access to permission $($permission.Name), will be executed during enforcement" - } - $existingRole = $currentRoles | Where-Object { $_.role -eq $roleName -and $_.resourceGroup -eq $resourceGroup } - - # Remove from current roles for later update - $null = $currentRoles.Remove($existingRole) - - $outputContext.AuditLogs.Add([PSCustomObject]@{ - Action = 'RevokePermission' - Message = "Revoked access to permission $($permission.Name)" - IsError = $false - }) - } - } - - # Actual update InPlanning account with desired roles - $correlatedAccount.roles = @($currentRoles) - $body = ($correlatedAccount | ConvertTo-Json -Depth 10) - $splatUpdateUserParams = @{ - Uri = "$($actionContext.Configuration.BaseUrl)/api/users" - Headers = $headers - Method = 'PUT' - Body = $body - ContentType = 'application/json;charset=utf-8' - } - if (-not($actionContext.DryRun -eq $true)) { - $null = Invoke-RestMethod @splatUpdateUserParams - } - - $outputContext.Success = $true - break - } - - 'NotFound' { - Write-Information "InPlanning account: [$($actionContext.References.Account)] could not be found, indicating that it may have been deleted" - $outputContext.Success = $false - $outputContext.AuditLogs.Add([PSCustomObject]@{ - Message = "InPlanning account: [$($actionContext.References.Account)] could not be found, indicating that it may have been deleted" - IsError = $true - }) - break - } - } -} -catch { - if ($outputContext.AuditLogs.Count -gt 0) { - $null = $outputContext.AuditLogs = [System.Collections.Generic.List[object]]::new() - } - - $outputContext.Success = $false - $ex = $PSItem - if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or - $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { - $errorObj = Resolve-InPlanningError -ErrorObject $ex - $auditMessage = "Could not manage InPlanning permissions. Error: $($errorObj.FriendlyMessage)" - Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" - } - else { - $auditMessage = "Could not manage InPlanning permissions. Error: $($_.Exception.Message)" - Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" - } - $outputContext.AuditLogs.Add([PSCustomObject]@{ - Message = $auditMessage - IsError = $true - }) -}#region functions -function Resolve-InPlanningError { - [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [object] - $ErrorObject - ) - process { - $httpErrorObj = [PSCustomObject]@{ - ScriptLineNumber = $ErrorObject.InvocationInfo.ScriptLineNumber - Line = $ErrorObject.InvocationInfo.Line - ErrorDetails = $ErrorObject.Exception.Message - FriendlyMessage = $ErrorObject.Exception.Message - } - if (-not [string]::IsNullOrEmpty($ErrorObject.ErrorDetails.Message)) { - $httpErrorObj.ErrorDetails = $ErrorObject.ErrorDetails.Message - } - elseif ($ErrorObject.Exception.GetType().FullName -eq 'System.Net.WebException') { - if ($null -ne $ErrorObject.Exception.Response) { - $streamReaderResponse = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd() - if (-not [string]::IsNullOrEmpty($streamReaderResponse)) { - $httpErrorObj.ErrorDetails = $streamReaderResponse - } - } - } - try { - $errorDetailsObject = ($httpErrorObj.ErrorDetails | ConvertFrom-Json) - if ($errorDetailsObject.error_description) { - $httpErrorObj.FriendlyMessage = $errorDetailsObject.error_description - } - else { - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails - } - } - catch { - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails - Write-Warning $_.Exception.Message - } - Write-Output $httpErrorObj - } -} - -function Get-AccessToken { - [CmdletBinding()] - param ( - ) - process { - try { - $tokenHeaders = [System.Collections.Generic.Dictionary[string, string]]::new() - $tokenHeaders.Add('Content-Type', 'application/x-www-form-urlencoded') - - $splatGetTokenParams = @{ - Uri = "$($actionContext.Configuration.BaseUrl)/api/token" - Headers = $tokenHeaders - Method = 'POST' - Body = @{ - client_id = $actionContext.Configuration.clientId - client_secret = $actionContext.Configuration.clientSecret - grant_type = 'client_credentials' - } - } - Write-Output (Invoke-RestMethod @splatGetTokenParams).access_token - } - catch { - $PSCmdlet.ThrowTerminatingError($_) - } - } -} - -function Resolve-ReplaceHolderValue { - param ( - [string] - $replaceVariable, - - [string] - $mappedProperty, - - $desiredPermission, - - $contract - ) - # Replace replace placeholder with actual value - if (-not [string]::IsNullOrEmpty($mappedProperty)) { - $keys = @($desiredPermission.Keys) - for ($i = 0; $i -lt $keys.Count; $i++) { - if ($desiredPermission[$keys[$i]] -like "*$($replaceVariable)*") { - $desiredPermission[$keys[$i]] = $desiredPermission[$keys[$i]] -replace ($replaceVariable, $mappedProperty) - } - } - } - else { - throw "Permission expects [$($replaceVariable)] to grant the permission but the specified value is empty for contract with id: [$($contract.ExternalId)]" - } -} -#endregion - -# Begin -try { - # Verify if [AccountReference] has a value - if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { - throw 'The account reference could not be found' - } - - if ($actionContext.Operation -ne 'revoke' ) { - $subPermission = $permissionMapping[$actionContext.References.Permission.Reference] - if ($null -eq $subPermission) { - throw "Permission [$($actionContext.References.Permission.Reference)] does not have a valid script mapping defined" - } - - $lookupValuesToCheck = $subPermission.Values -match '\{\{[^}]+\}\}' - foreach ($replaceVariable in $lookupValuesToCheck) { - if ($replaceVariable -notin $lookupValues.keys ) { - throw "Permission [$($actionContext.References.Permission.Reference)] expects a value for [$($replaceVariable)], but it was not provided as script lookup value" - } - } - } - - # Set Headers for API calls - $accessToken = Get-AccessToken - $headers = [System.Collections.Generic.Dictionary[string, string]]::new() - $headers.Add('Content-Type', 'application/json') - $headers.Add('Authorization', 'Bearer ' + $accessToken) - - Write-Information 'Verifying if a InPlanning account exists' - try { - $splatGetUserParams = @{ - Uri = "$($actionContext.Configuration.BaseUrl)/api/users/$($actionContext.References.Account)" - Headers = $headers - Method = 'GET' - } - $correlatedAccount = Invoke-RestMethod @splatGetUserParams - } - catch { - if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { - $correlatedAccount = $null - } - } - - if ($null -ne $correlatedAccount) { - $lifecycleProcess = 'ManageSubPermissions' - $currentRoles = [System.Collections.Generic.List[object]]::new() - if ($null -ne $correlatedAccount.roles) { - [System.Collections.Generic.List[object]]$currentRoles = $correlatedAccount.roles.PSObject.copy() - } - } - else { - $lifecycleProcess = 'NotFound' - } - - switch ($lifecycleProcess) { - 'ManageSubPermissions' { - # Collect current permissions - $currentPermissions = @{} - foreach ($permission in $actionContext.CurrentPermissions) { - $currentPermissions[$permission.Reference.Id] = $subPermission - } - - # Collect and calculate desired permissions - $desiredPermissions = @{} - if (-not($actionContext.Operation -eq 'revoke')) { - - # Processing Dynamic permissions body with placeholder(s) - if ($subPermission.Values -match '\{\{[^}]+\}\}') { - Write-Information "Permission [$($actionContext.References.Permission.Reference)] contains placeholder values which need to be resolved" - foreach ($contract in $personContext.Person.Contracts) { - if ($contract.Context.InConditions -or ($actionContext.DryRun -eq $true)) { - $desiredPermission = $subPermission.PSObject.Copy() - foreach ($replaceVariable in $lookupValues.GetEnumerator()) { - # Perform lookup in HelloId contract for the correct - $lookupValue = $replaceVariable.Value - $mappedProperty = ($contract | Select-Object $lookupValue).$lookupValue - $null = Resolve-ReplaceHolderValue -ReplaceVariable $replaceVariable.Key -MappedProperty $mappedProperty -Contract $contract -DesiredPermission $desiredPermission - } - $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" - $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission - } - } - } - - # Processing Static permissions body without placeholder(s) - else { - $desiredPermission = $subPermission.PSObject.Copy() - $desiredPermissionUniqueKey = "$($desiredPermission.Role)&&$($desiredPermission.ResourceGroup)" - $desiredPermissions[$desiredPermissionUniqueKey] = $desiredPermission - } - - # Process desired permissions calculation Grant and Update - foreach ($permission in $desiredPermissions.GetEnumerator()) { - $outputContext.SubPermissions.Add([PSCustomObject]@{ - DisplayName = $permission.Name - Reference = [PSCustomObject]@{ - Id = $permission.Name - } - }) - if (-not $currentPermissions.ContainsKey($permission.Name)) { - if ($actionContext.DryRun -eq $true) { - Write-Information "[DryRun] Grant access to permission $($permission.Name), will be executed during enforcement" - } - + $existingRole = $currentRoles | Where-Object { $_.role -eq $permission.Value.role -and $_.resourceGroup -eq $permission.Value.resourceGroup } if (-not $existingRole) { $null = $currentRoles.Add($permission.value) From 3a72d27783d37169369ddb67aa1bda4fb8542fe9 Mon Sep 17 00:00:00 2001 From: rhouthuijzen <116062840+rhouthuijzen@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:57:47 +0200 Subject: [PATCH 6/9] Update and rename readme.md to README.md --- readme.md => README.md | 3 --- 1 file changed, 3 deletions(-) rename readme.md => README.md (96%) diff --git a/readme.md b/README.md similarity index 96% rename from readme.md rename to README.md index 1e73436..bf59632 100644 --- a/readme.md +++ b/README.md @@ -140,9 +140,6 @@ $lookupValues = @{ > [!TIP] > _For more information on how to configure a HelloID PowerShell connector, please refer to our [documentation](https://docs.helloid.com/en/provisioning/target-systems/powershell-v2-target-systems.html) pages_. -> [!TIP] -> _If you need help, feel free to ask questions on our [forum](https://forum.helloid.com/forum/helloid-connectors/provisioning/1481-helloid-conn-prov-target-intus)_ - ## HelloID docs The official HelloID documentation can be found at: https://docs.helloid.com/ From a96d3c9031544649c8e0eae3b064cab5d52d8b0a Mon Sep 17 00:00:00 2001 From: rhouthuijzen <116062840+rhouthuijzen@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:58:39 +0200 Subject: [PATCH 7/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf59632..99bec03 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- +

From e08af7d7c8d23188085e1558300a3aa61d152a8e Mon Sep 17 00:00:00 2001 From: rhouthuijzen <116062840+rhouthuijzen@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:13:22 +0200 Subject: [PATCH 8/9] Fixed #8 --- create.ps1 | 2 +- disable.ps1 | 2 +- enable.ps1 | 2 +- permissions/subPermissions.ps1 | 2 +- update.ps1 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/create.ps1 b/create.ps1 index 6b6d777..077e9e6 100644 --- a/create.ps1 +++ b/create.ps1 @@ -98,7 +98,7 @@ try { $correlatedAccount = Invoke-RestMethod @splatGetUserParams } catch { - if (-not($_.ErrorDetails.Message -match '211 - Object does not exist')) { + if (-not($_.ErrorDetails.Message -match '211 - .*does not exist')) { throw "Cannot get user error: [$($_.Exception.Message)]" } } diff --git a/disable.ps1 b/disable.ps1 index 000bd4e..cec74d6 100644 --- a/disable.ps1 +++ b/disable.ps1 @@ -86,7 +86,7 @@ try { } $correlatedAccount = Invoke-RestMethod @splatGetUserParams } catch { - if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { + if ( -not ($_.ErrorDetails.Message -match '211 - .*does not exist')) { throw "Cannot get user error: [$($_.Exception.Message)]" } } diff --git a/enable.ps1 b/enable.ps1 index 68b6bcd..9dbb116 100644 --- a/enable.ps1 +++ b/enable.ps1 @@ -86,7 +86,7 @@ try { } $correlatedAccount = Invoke-RestMethod @splatGetUserParams } catch { - if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { + if ( -not ($_.ErrorDetails.Message -match '211 - .*does not exist')) { throw "Cannot get user error: [$($_.Exception.Message)]" } } diff --git a/permissions/subPermissions.ps1 b/permissions/subPermissions.ps1 index bc3908d..95978bb 100644 --- a/permissions/subPermissions.ps1 +++ b/permissions/subPermissions.ps1 @@ -182,7 +182,7 @@ try { $correlatedAccount = Invoke-RestMethod @splatGetUserParams } catch { - if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { + if ( -not ($_.ErrorDetails.Message -match '211 - .*does not exist')) { $correlatedAccount = $null } } diff --git a/update.ps1 b/update.ps1 index 5590edc..3786a68 100644 --- a/update.ps1 +++ b/update.ps1 @@ -87,7 +87,7 @@ try { } $correlatedAccount = Invoke-RestMethod @splatGetUserParams } catch { - if ( -not ($_.ErrorDetails.Message -match '211 - Object does not exist')) { + if ( -not ($_.ErrorDetails.Message -match '211 - .*does not exist')) { throw "Cannot get user error: [$($_.Exception.Message)]" } } From f1afee5f54c1457b979ca14f8d2373c8d71f6828 Mon Sep 17 00:00:00 2001 From: rhouthuijzen <116062840+rhouthuijzen@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:17:24 +0200 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045aa91..2991704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ All notable changes to this project will be documented in this file. The format ### Changed - Support '-' in permission names - Made permission name and role name independant -- Minor fixes +- Minor fixes + +### Fixed +- Issue #8 ## [3.0.1] - 01-05-2026