From 08d6451d236abac8bbedc459abbb8eee50661a3a Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:06:49 +0200 Subject: [PATCH 01/10] PSv2 version Powershell v2 --- configuration.json | 76 ++++++------ create.ps1 | 298 +++++++++++++++++++++++++++++---------------- delete.ps1 | 178 +++++++++++++++------------ enable.ps1 | 118 ++++++++++++++++++ fieldmapping.json | 120 ++++++++++++++++++ readme.md | 39 ++++-- update.ps1 | 223 +++++++++++++++++---------------- 7 files changed, 715 insertions(+), 337 deletions(-) create mode 100644 enable.ps1 create mode 100644 fieldmapping.json diff --git a/configuration.json b/configuration.json index ed3b313..ba24166 100644 --- a/configuration.json +++ b/configuration.json @@ -1,42 +1,42 @@ [ - { - "key": "NavAdminToolsFile", - "type": "input", - "defaultValue": "", - "templateOptions": { - "label": "NavAdminToolsFile", - "description": "The full path to the NavAdminToolsFile *.ps1 file", - "required": true - } + { + "defaultValue": "", + "key": "Applicationserver", + "templateOptions": { + "description": "Server containing NAV module", + "label": "Applicationserver", + "required": true }, - { - "key": "ServerInstance", - "type": "input", - "defaultValue": "", - "templateOptions": { - "label": "ServerInstance", - "description": "The name of the Dynamics Nav server instance.", - "required": true - } + "type": "input" + }, + { + "defaultValue": "", + "key": "PSModulePath", + "templateOptions": { + "description": "", + "label": "Powershell module path", + "required": true }, - { - "key": "Tenant", - "type": "input", - "defaultValue": "", - "templateOptions": { - "label": "Tenant", - "description": "The ID of the Dynamics Nav tenant.", - "required": true - } + "type": "input" + }, + { + "defaultValue": "", + "key": "NavServerInstanceName", + "templateOptions": { + "description": "NavServer Instance Name", + "label": "NavServer Instance Name", + "required": true }, - { - "key": "IsDebug", - "type": "checkbox", - "defaultValue": true, - "templateOptions": { - "label": "Toggle debug logging", - "description": "When toggled, debug logging will be displayed", - "required": false - } - } -] + "type": "input" + }, + { + "defaultValue": "", + "key": "Tenant", + "templateOptions": { + "description": "Tenant", + "label": "Tenant", + "required": true + }, + "type": "input" + } +] \ No newline at end of file diff --git a/create.ps1 b/create.ps1 index 7b5cc67..562c1f5 100644 --- a/create.ps1 +++ b/create.ps1 @@ -1,130 +1,216 @@ -################################################ -# HelloID-Conn-Prov-Target-DynamicsEmpire-Create -# -# Version: 1.0.0 -################################################ -# Initialize default values -$config = $configuration | ConvertFrom-Json -$p = $person | ConvertFrom-Json -$success = $false -$auditLogs = [Collections.Generic.List[PSCustomObject]]::new() - -# Default company -if($p.PrimaryContract.CostCenter.Code -eq ''){ - $defaultCompany = '' -} elseif($p.PrimaryContract.CostCenter.Code -eq ''){ - $defaultCompany = '' -} - -# FullName -if ($null -ne $p.custom.GewTussenvoegsel) { - $fullName = $firstName + " " + $middleName + " " + $lastName -} else { - $fullName = $firstName + " " + $lastName -} +################################################# +# HelloID-Conn-Prov-Target-Empire-Create +# PowerShell V2 +################################################# -# Account mapping -$account = [PSCustomObject]@{ - displayName = $fullName - firstName = $p.Name.NickName - middleName = $p.custom.GewTussenvoegsel - lastName = $p.custom.GewAchternaam - emailAddress = $p.accounts.MicrosoftActiveDirectory.mail - samAccountName = $p.Contact.Business.Email #$p.Accounts.MicrosoftActiveDirectory.sAMAccountName - userPrincipalName = $p.Accounts.MicrosoftActiveDirectory.userPrincipalName - defaultCompany = $defaultCompany - languageID = '' - permissionSetId = '' -} +# Enable TLS1.2 +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -# Set debug logging -switch ($($config.IsDebug)) { - $true { $VerbosePreference = 'Continue' } - $false { $VerbosePreference = 'SilentlyContinue' } +#region functions +function Resolve-EmpireError { + [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) + # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. + # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment + } + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + } + Write-Output $httpErrorObj + } } +#endregion try { - Write-Verbose "Importing NavAdminToolsFile: [$($config.NavAdminToolsFile)]" - if(-not(Test-Path $($config.NavAdminToolsFile))){ - throw [System.IO.FileNotFoundException]("The file: [$($config.NavAdminToolsFile)] cannot be found") - } else { - # Dotsource the *.ps1 file to make sure its function are imported in the current session - .$($config.NavAdminToolsFile) + # Create session + $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver + + # Define variables + $EmailAddress = $actionContext.Data.EmailAddress + $SamAccountName = $actionContext.Data.Username + $UserPrincipalName = $actionContext.Data.userPrincipalName + $Displayname = $actionContext.Data.Displayname + + $PSPath = $actionContext.Configuration.PSModulePath + $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName + $tenant = $actionContext.Configuration.Tenant + + # Import module + $Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction Stop + } -ArgumentList $PSPath + + # Initial assignment + $outputContext.AccountReference = 'Currently not available' + + # Determine action (Create or Correlate) + if ($actionContext.CorrelationConfiguration.Enabled) { + try { + $correlatedAccount = Invoke-Command -Session $s -ScriptBlock { + param($ServerInstance, $Tenant, $UserName) + Get-NAVServerUser -ServerInstance $ServerInstance -Tenant $Tenant | + Where-Object { $_.UserName -eq $UserName } + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName + + } + catch { + throw "Error while checking for existing NAV user: $($_.Exception.Message)" + } + } + else { + throw "Correlation for this connector is mandatory" } - Write-Verbose "Verifying if DynamicsEmpire account for [$($p.DisplayName)] must be created or correlated" - $splatGetNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) + if ($null -ne $correlatedAccount) { + $action = 'CorrelateAccount' } - $navUser = Get-NAVServerUser @splatGetNavUserParams -ErrorAction SilentlyContinue -Verbose | Where-Object {$_.UserName -eq $($account.samAccountName)} - if (-not($navUser)) { - $action = 'Create-Correlate' - } else { - $action = 'Correlate' + else { + $action = 'CreateAccount' } - # Add a warning message showing what will happen during enforcement - if ($dryRun -eq $true) { - Write-Information "[DryRun] $action DynamicsEmpire account for: [$($p.DisplayName)], will be executed during enforcement" + # Add a message and the result of each of the validations showing what will happen during enforcement + if ($actionContext.DryRun -eq $true) { + Write-Information "[DryRun] $action Authorizationbox account for: [$($personContext.Person.DisplayName)], will be executed during enforcement" } - # Process - if (-not($dryRun -eq $true)) { - switch ($action){ - 'Create-Correlate'{ - Write-Verbose "Creating and correlating DynamicsEmpire account" - $splatCreateNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - WindowsAccount = $($account.samAccountName) - FullName = $($account.displayName) - AuthenticationEmail = $($account.userPrincipalName) - Company = $($account.defaultCompany) - LanguageID = $($account.languageID) - ContactEmail = $($account.emailAddress) - } - $null = New-NAVServerUser @splatCreateNavUserParams -ErrorAction Stop -Verbose + switch ($action) { + 'CreateAccount' { + Write-Information 'Creating Nav User' - $splatCreateNavPermission = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - WindowsAccount = $($account.samAccountName) - PermissionSetId = $($account.permissionSetId) - } + if (-not($actionContext.DryRun -eq $true)) { + #Create user + $createUser = Invoke-Command -Session $s -ScriptBlock { + param( + $ServerInstance, + $Tenant, + $SamAccountName, + $Displayname, + $UserPrincipalName, + $EmailAddress + ) + New-NAVServerUser ` + -ServerInstance $ServerInstance ` + -Tenant $Tenant ` + -WindowsAccount $SamAccountName ` + -FullName $Displayname ` + -AuthenticationEmail $UserPrincipalName ` + -ContactEmail $EmailAddress ` + -State Disabled ` + -Verbose + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Displayname, $UserPrincipalName, $EmailAddress - $null = New-NAVServerUserPermissionSet @splatCreateNavPermission -ErrorAction Stop -Verbose - $accountReference = (New-Guid).guid - } + #Set permissions + <# + $setPermissions = Invoke-Command -Session $s -ScriptBlock { + param( + $ServerInstance, + $Tenant, + $SamAccountName, + $PermissionSetId + ) + New-NAVServerUserPermissionSet ` + -ServerInstance $ServerInstance ` + -Tenant $Tenant ` + -WindowsAccount $SamAccountName ` + -PermissionSetId $PermissionSetId ` + -ErrorAction SilentlyContinue ` + -Verbose + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, 'SUPER' + #> + + #Try another get user for setting aref + $result = Invoke-Command -Session $s -ScriptBlock { + param($ServerInstance, $Tenant, $UserName) + Get-NAVServerUser -ServerInstance $ServerInstance -Tenant $Tenant | + Where-Object { $_.UserName -eq $UserName } + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName + + $outputContext.Data.SecurityID = $result.UserSecurityID + $outputContext.Data.DisplayName = $result.Fullname + $outputContext.Data.UserName = $result.Username - 'Correlate'{ - Write-Verbose "Creating and correlating DynamicsEmpire account" - $accountReference = (New-Guid).guid + $auditLogMessage = "Create account was successful. AccountReference is: [$($outputContext.Data.SecurityID)]" + } + else { + Write-Information 'Dryrun prevented this action' + } + break + } + + 'CorrelateAccount' { + Write-Information 'Correlating Nav User' + + $outputContext.Data.SecurityID = $correlatedAccount.UserSecurityID + $outputContext.Data.DisplayName = $correlatedAccount.Fullname + $outputContext.Data.UserName = $correlatedAccount.Username + + $outputContext.AccountCorrelated = $true + $auditLogMessage = "Correlated account: [$($actionContext.Data.Username)]. AccountReference is: [$($outputContext.Data.SecurityID)]" + break } + } - $success = $true - $auditLogs.Add([PSCustomObject]@{ - Message = "$action DynamicsEmpire account was successful. AccountReference is: [$accountReference]" - IsError = $false - }) + $accountRef = [PSCustomObject]@{ + SecurityID = $outputContext.Data.SecurityID + DisplayName = $outputContext.Data.DisplayName + UserName = $outputContext.Data.UserName } -} catch { - $success = $false + + $outputContext.AccountReference = $accountRef + $outputContext.Success = $true + + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Action = $action + Message = $auditLogMessage + IsError = $false + }) + + # Remove session + $exit = if ($s) { Remove-PSSession $s } +} +catch { + $outputContext.Success = $false $ex = $PSItem - $auditMessage = "Could not create DynamicsEmpire account. Error: $($ex.Exception.Message)" - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" - $auditLogs.Add([PSCustomObject]@{ + $exit = if ($s) { Remove-PSSession $s } + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-EmpireError -ErrorObject $ex + $auditMessage = "Could not create or correlate Empire account. Error: $($errorObj.FriendlyMessage)" + Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Could not create or correlate Empire account. Error: $($ex.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 }) -} finally { - $result = [PSCustomObject]@{ - Success = $success - AccountReference = $accountReference - AuditLogs = $auditLogs - Account = $account - - } - Write-Output ($result | ConvertTo-Json -Depth 10) } diff --git a/delete.ps1 b/delete.ps1 index c727e0c..9820210 100644 --- a/delete.ps1 +++ b/delete.ps1 @@ -1,94 +1,118 @@ -################################################ -# HelloID-Conn-Prov-Target-DynamicsEmpire-Delete -# -# Version: 1.0.0 -################################################ -# Initialize default values -$config = $configuration | ConvertFrom-Json -$p = $person | ConvertFrom-Json -$aRef = $AccountReference | ConvertFrom-Json -$success = $false -$auditLogs = [System.Collections.Generic.List[PSCustomObject]]::new() +################################################## +# HelloID-Conn-Prov-Target-Empire-Delete +# PowerShell V2 +################################################## -# Set debug logging -switch ($($config.IsDebug)) { - $true { $VerbosePreference = 'Continue' } - $false { $VerbosePreference = 'SilentlyContinue' } +# Enable TLS1.2 +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + +#region functions +function Resolve-EmpireError { + [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) + # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. + # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment + } + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + } + Write-Output $httpErrorObj + } } +#endregion try { - if(-not(Test-Path $($config.NavAdminToolsFile))){ - throw [System.IO.FileNotFoundException]("The file: [$($config.NavAdminToolsFile)] cannot be found") - } else { - # Dotsource the *.ps1 file to make sure its function are imported in the current session - .$($config.NavAdminToolsFile) + # Verify if [aRef] has a value + if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { + throw 'The account reference could not be found' } + + # Create session + $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver - Write-Verbose "Verifying if a DynamicsEmpire account for [$($p.DisplayName)] exists" - $splatGetNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - } - $navUser = Get-NAVServerUser @splatGetNavUserParams -ErrorAction SilentlyContinue -Verbose | Where-Object {$_.UserName -eq $($account.samAccountName)} + # Define variables + $SamAccountName = $actionContext.Data.Username + $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName + $PSPath = $actionContext.Configuration.PSModulePath + $tenant = $actionContext.Configuration.Tenant - if ($null -ne $navUser) { - $action = 'Found' - $dryRunMessage = "Delete DynamicsEmpire account for: [$($p.DisplayName)] will be executed during enforcement" - } - elseif ($null -eq $navUser) { - $action = 'NotFound' - $dryRunMessage = "DynamicsEmpire account for: [$($p.DisplayName)] not found. Possibly already deleted. Skipping action" - } + # Import module + $Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction Stop + } -ArgumentList $PSPath - # Add an auditMessage showing what will happen during enforcement - if ($dryRun -eq $true) { - Write-Information "[DryRun] $dryRunMessage" - } + $action = 'DeleteAccount' - # Process - if (-not($dryRun -eq $true)) { - Write-Verbose "Deleting DynamicsEmpire account with accountReference: [$aRef]" - - switch ($action){ - 'Found'{ - $splatDeleteNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - WindowsAccount = $p.Accounts.MicrosoftActiveDirectory.sAMAccountName - State = 'Disabled' - } - $null = Set-NAVServerUser @splatDeleteNavUserParams -ErrorAction Stop -Verbose -Force - $auditLogs.Add([PSCustomObject]@{ - Message = 'Delete account was successful' - IsError = $false - }) - break + switch ($action) { + 'DeleteAccount' { + if (-not($actionContext.DryRun -eq $true)) { + # Disable user + Invoke-Command -Session $s -ScriptBlock { + param($ServerInstance, $Tenant, $UserName) + Set-NAVServerUser ` + -ServerInstance $ServerInstance ` + -Tenant $Tenant ` + -WindowsAccount $UserName ` + -State Disabled ` + -ErrorAction Stop ` + -Verbose + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName + } - - 'NotFound'{ - $auditLogs.Add([PSCustomObject]@{ - Message = "DynamicsEmpire account for: [$($p.DisplayName)] not found. Possibly already deleted. Skipping action" - IsError = $false - }) - break + else { + Write-Information "[DryRun] Would delete (disable) NAV user [$SamAccountName]" } } - - $success = $true } -} catch { - $success = $false + + $outputContext.Success = $true + + #Remove session + $exit = if ($s) { Remove-PSSession $s } + +} +catch { + $outputContext.success = $false $ex = $PSItem - $auditMessage = "Could not delete DynamicsEmpire account. Error: $($ex.Exception.Message)" - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" - $auditLogs.Add([PSCustomObject]@{ + $exit = if ($s) { Remove-PSSession $s } + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-EmpireError -ErrorObject $ex + $auditMessage = "Could not delete Empire account. Error: $($errorObj.FriendlyMessage)" + Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Could not delete Empire account. Error: $($ex.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 }) -} finally { - $result = [PSCustomObject]@{ - Success = $success - Auditlogs = $auditLogs - } - Write-Output $result | ConvertTo-Json -Depth 10 -} +} \ No newline at end of file diff --git a/enable.ps1 b/enable.ps1 new file mode 100644 index 0000000..01d8028 --- /dev/null +++ b/enable.ps1 @@ -0,0 +1,118 @@ +################################################## +# HelloID-Conn-Prov-Target-Authorizationbox-Enable +# PowerShell V2 +################################################## + +# Enable TLS1.2 +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 + +#region functions +function Resolve-EmpireError { + [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) + # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. + # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment + } + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + } + Write-Output $httpErrorObj + } +} +#endregion + +try { + # Verify if [aRef] has a value + if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { + throw 'The account reference could not be found' + } + + # Create session + $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver + + # Define variables + $SamAccountName = $actionContext.Data.Username + $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName + $PSPath = $actionContext.Configuration.PSModulePath + $tenant = $actionContext.Configuration.Tenant + + # Import module + $Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction Stop + } -ArgumentList $PSPath + + $action = 'EnableAccount' + + switch ($action) { + 'EnableAccount' { + if (-not($actionContext.DryRun -eq $true)) { + # Enable user + Invoke-Command -Session $s -ScriptBlock { + param($ServerInstance, $Tenant, $UserName) + Set-NAVServerUser ` + -ServerInstance $ServerInstance ` + -Tenant $Tenant ` + -WindowsAccount $UserName ` + -State Enabled ` + -ErrorAction Stop ` + -Verbose + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName + + } + else { + Write-Information "[DryRun] Would enable NAV user [$SamAccountName]" + } + } + } + + $outputContext.Success = $true + + #Remove session + $exit = if ($s) { Remove-PSSession $s } + +} +catch { + $outputContext.success = $false + $ex = $PSItem + $exit = if ($s) { Remove-PSSession $s } + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-EmpireError -ErrorObject $ex + $auditMessage = "Could not enable Empire account. Error: $($errorObj.FriendlyMessage)" + Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Could not enable Empire account. Error: $($ex.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 + }) +} \ No newline at end of file diff --git a/fieldmapping.json b/fieldmapping.json new file mode 100644 index 0000000..806c6b4 --- /dev/null +++ b/fieldmapping.json @@ -0,0 +1,120 @@ +{ + "Version": "v1", + "MappingFields": [ + { + "Name": "DefaultCompany", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Complex", + "Value": "\"function getValue(){\\r\\n let costcenter = Person.PrimaryContract.CostCenter.Code\\r\\n let company = ''\\r\\n\\r\\n return company\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "UsedInNotifications": false, + "StoreInAccountData": false + } + ] + }, + { + "Name": "Username", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Complex", + "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n \\r\\n let domain = 'BBINST\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "UsedInNotifications": false, + "StoreInAccountData": true + } + ] + }, + { + "Name": "userPrincipalName", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Complex", + "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.userPrincipalName\\r\\n \\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "UsedInNotifications": false, + "StoreInAccountData": false + } + ] + }, + { + "Name": "EmailAddress", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Complex", + "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.mail\\r\\n\\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "UsedInNotifications": false, + "StoreInAccountData": true + } + ] + }, + { + "Name": "DisplayName", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Complex", + "Value": "\"function generateDisplayName() {\\r\\n\\r\\n let nickName = Person.Name.NickName;\\r\\n let middleName = Person.Name.FamilyNamePrefix;\\r\\n let lastName = Person.Name.FamilyName;\\r\\n let middleNamePartner = Person.Name.FamilyNamePartnerPrefix;\\r\\n let lastNamePartner = Person.Name.FamilyNamePartner;\\r\\n let convention = Person.Name.Convention;\\r\\n\\r\\n let displayName = '';\\r\\n switch (convention) {\\r\\n case \\\"BP\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n\\r\\n displayName = displayName + ' - ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n break;\\r\\n case \\\"PB\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n\\r\\n displayName = displayName + ' - ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n break;\\r\\n case \\\"P\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n break;\\r\\n case \\\"B\\\":\\r\\n default:\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n break;\\r\\n }\\r\\n // Trim spaces at start and end\\r\\n displayName = displayName.trim();\\r\\n\\r\\n // Shorten string to maxAttributeLength \\r\\n const maxAttributeLength = 256;\\r\\n displayName = displayName.substring(0, maxAttributeLength);\\r\\n \\r\\n return displayName;\\r\\n}\\r\\n\\r\\ngenerateDisplayName();\"", + "UsedInNotifications": false, + "StoreInAccountData": true + } + ] + }, + { + "Name": "SecurityID", + "Description": "", + "Type": "Text", + "MappingActions": [ + { + "MapForActions": [ + "Create", + "Update", + "Delete", + "Enable" + ], + "MappingMode": "Fixed", + "Value": "\"\"", + "UsedInNotifications": false, + "StoreInAccountData": true + } + ] + } + ], + "UniqueFieldNames": [] +} \ No newline at end of file diff --git a/readme.md b/readme.md index 6d5817a..2cd23f1 100644 --- a/readme.md +++ b/readme.md @@ -35,8 +35,9 @@ The following lifecycle events are available: | Event | Description | Notes | |--- |--- |--- | | create.ps1 | Create and/or correlate an Account | - | +| enable.ps1 | Enable the Account | - | | update.ps1 | Update the Account | - | -| delete.ps1 | Delete the Account | - | +| delete.ps1 | Delete (Disable) the Account | - | ## Getting started @@ -46,24 +47,42 @@ The following settings are required to connect to the API. | Setting | Description | Mandatory | | ------------ | ----------- | ----------- | -| NavAdminToolsFile| The full path to the NavAdminToolsFile *.ps1 file| Yes -| ServerInstance | The name of the Dynamics Nav server instance | Yes | +| PSModulePath | The full path to the NavAdminToolsFile *.ps1 file| Yes +| Applicationserver | The name of the Dynamics Nav application server | Yes | +| NavServerInstanceName | The name of the Dynamics Nav server instance | Yes | | Tenant | The ID of the Dynamics Nav tenant | Yes +#### Correlation configuration + +The correlation configuration is used to specify which properties will be used to match an existing account within _Empire_ to a person in _HelloID_. + +To properly setup the correlation: + +1. Open the `Correlation` tab. + +2. Specify the following configuration: + + | Setting | Value | + | ------------------------- | ------------ | + | Enable correlation | `True` | + | Person correlation field | `` | + | Account correlation field | `Username` | + + ### Remarks +- Service user that runs the agent must have sufficient rights within Empire to create/update/delete accounts. +- This connector is usually linked to the AuthorizationBox connector, as Security ID is required there. #### Dotsourcing of the _`NavAdminTools.ps1`_ file The `NavAdminTools.ps1` file is dotsourced in all lifecycle actions. Meaning that the functions defined within the _*.ps1_ file are imported into the current session. The original code was based on a _PSSession_ with `Import-Module` and `Invoke-Command`. Since the _*.ps1_ file is not a module, this seems to be unnecessary but requires further testing. ```powershell - $s = New-PSSession -ComputerName 'applicationServer' - Invoke-Command -Session $s -ScriptBlock { - $ImportModule = Import-Module "D:\NavAdminTool.ps1" -ErrorAction SilentlyContinue - Invoke-Command -Session $s -ScriptBlock { - $CreateNavUser = New-NAVServerUser -ServerInstance $using:NavServerInstanceName -Tenant $using:tenant -WindowsAccount $using:SamAccountName -FullName $using:DisplayName -AuthenticationEmail $using:UserPrincipalName -Company $using:DefaultCompany -LanguageID $using:LanguageID -ContactEmail $using:EmailAddress -ErrorAction SilentlyContinue -Verbose - } - } + # Import module + $Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction Stop + } -ArgumentList $PSPath ``` ## Setup the connector diff --git a/update.ps1 b/update.ps1 index 481130e..5274fa3 100644 --- a/update.ps1 +++ b/update.ps1 @@ -1,128 +1,139 @@ -################################################ +################################################# # HelloID-Conn-Prov-Target-DynamicsEmpire-Update -# -# Version: 1.0.0 -################################################ -# Initialize default values -$config = $configuration | ConvertFrom-Json -$p = $person | ConvertFrom-Json -$aRef = $AccountReference | ConvertFrom-Json -$success = $false -$auditLogs = [System.Collections.Generic.List[PSCustomObject]]::new() +# PowerShell V2 +################################################# -# Default company -if($p.PrimaryContract.CostCenter.Code -eq ''){ - $defaultCompany = '' -} elseif($p.PrimaryContract.CostCenter.Code -eq ''){ - $defaultCompany = '' -} - -# FullName -if ($null -ne $p.custom.GewTussenvoegsel) { - $fullName = $firstName + " " + $middleName + " " + $lastName -} else { - $fullName = $firstName + " " + $lastName -} +# Enable TLS1.2 +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -# Account mapping -$account = [PSCustomObject]@{ - displayName = $fullName - firstName = $p.Name.NickName - middleName = $p.custom.GewTussenvoegsel - lastName = $p.custom.GewAchternaam - emailAddress = $p.accounts.MicrosoftActiveDirectory.mail - samAccountName = $p.Accounts.MicrosoftActiveDirectory.sAMAccountName - userPrincipalName = $p.Accounts.MicrosoftActiveDirectory.userPrincipalName - defaultCompany = $defaultCompany - languageID = '' - permissionSetId = '' -} - -# Set debug logging -switch ($($config.IsDebug)) { - $true { $VerbosePreference = 'Continue' } - $false { $VerbosePreference = 'SilentlyContinue' } +#region functions +function Resolve-EmpireError { + [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) + # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. + # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment + } + catch { + $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails + } + Write-Output $httpErrorObj + } } +#endregion try { - if(-not(Test-Path $($config.NavAdminToolsFile))){ - throw [System.IO.FileNotFoundException]("The file: [$($config.NavAdminToolsFile)] cannot be found") - } else { - # Dotsource the *.ps1 file to make sure its function are imported in the current session - .$($config.NavAdminToolsFile) + # Verify if [aRef] has a value + if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { + throw 'The account reference could not be found' } - Write-Verbose "Verifying if a DynamicsEmpire account for [$($p.DisplayName)] exists" - $splatGetNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - } - $navUser = Get-NAVServerUser @splatGetNavUserParams -ErrorAction SilentlyContinue -Verbose | Where-Object {$_.UserName -eq $($account.samAccountName)} + # Create session + $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver - if ($null -ne $navUser) { - $action = 'Update' - $dryRunMessage = "DynamicsEmpire account for: [$($p.DisplayName)] will be updated." - } - elseif ($null -eq $navUser) { - $action = 'NotFound' - $dryRunMessage = "DynamicsEmpire account for: [$($p.DisplayName)] not found. Possibly deleted." - } + # Define variables + $SamAccountName = $actionContext.Data.Username + $Displayname = $actionContext.Data.DisplayName + $UserPrincipalName = $actionContext.Data.userPrincipalName + $EmailAddress = $actionContext.Data.EmailAddress + + $PSPath = $actionContext.Configuration.PSModulePath + $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName + $tenant = $actionContext.Configuration.Tenant - # Add a warning message showing what will happen during enforcement - if ($dryRun -eq $true) { - Write-Information "[DryRun] $dryRunMessage" - } + # Make sure module is imported + $Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction SilentlyContinue + } -ArgumentList $PSPath - # Process - if (-not($dryRun -eq $true)) { - switch ($action) { - 'Update' { - Write-Verbose "Updating DynamicsEmpire account with accountReference: [$aRef]" - $splatSetNavUserParams = @{ - ServerInstance = $($config.ServerInstance) - Tenant = $($config.Tenant) - WindowsAccount = $($account.samAccountName) - FullName = $($account.displayName) - AuthenticationEmail = $($account.userPrincipalName) - Company = $($account.defaultCompany) - LanguageID = $($account.languageID) - ContactEmail = $($account.emailAddress) - } + # Compare can be made but Nav handles this. We keep this $action variable for when we want to expend on the actions. + $action = 'UpdateAccount' - $null = Set-NAVServerUser @splatSetNavUserParams -ErrorAction Stop -Verbose -Force + switch ($action) { + 'UpdateAccount' { + Write-Information "Updating Authorizationbox account with accountReference: [$($actionContext.References.Account)]" - $success = $true - $auditLogs.Add([PSCustomObject]@{ - Message = 'Update DynamicsEmpire account was successful' - IsError = $false - }) - break + if (-not($actionContext.DryRun -eq $true)) { + $UpdateAction = Invoke-Command -Session $s -ScriptBlock { + param( + $ServerInstance, + $Tenant, + $SamAccountName, + $Displayname, + $UserPrincipalName, + $EmailAddress + ) + Set-NAVServerUser ` + -ServerInstance $ServerInstance ` + -Tenant $Tenant ` + -WindowsAccount $SamAccountName ` + -FullName $Displayname ` + -AuthenticationEmail $UserPrincipalName ` + -ContactEmail $EmailAddress ` + -ErrorAction Inquire ` + -Verbose ` + -Force + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Displayname, $UserPrincipalName, $EmailAddress } - - 'NotFound' { - $success = $false - $auditLogs.Add([PSCustomObject]@{ - Message = "DynamicsEmpire account for: [$($p.DisplayName)] not found. Possibly deleted" - IsError = $true - }) - break + else { + Write-Information "Dryrun prevented this action" } + } } -} catch { - $success = $false + + $outputContext.Data.SecurityID = $($actionContext.References.Account.SecurityID) + + $outputContext.Success = $true + $outputContext.AuditLogs.Add([PSCustomObject]@{ + Message = "Update account was successfull" + IsError = $false + }) + + #Remove session + $exit = Remove-PSSession $s +} +catch { + $outputContext.Success = $false $ex = $PSItem - $auditMessage = "Could not update DynamicsEmpire account. Error: $($ex.Exception.Message)" - Write-Verbose "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" - $auditLogs.Add([PSCustomObject]@{ + $exit = Remove-PSSession $s + if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or + $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { + $errorObj = Resolve-EmpireError -ErrorObject $ex + $auditMessage = "Could not update Empire account. Error: $($errorObj.FriendlyMessage)" + Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" + } + else { + $auditMessage = "Could not update Empire account. Error: $($ex.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 }) -} finally { - $result = [PSCustomObject]@{ - Success = $success - Account = $account - Auditlogs = $auditLogs - } - Write-Output $result | ConvertTo-Json -Depth 10 } From f0833d812e0d4ddfa3bd2df281981eea7bdb95a0 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:53:30 +0200 Subject: [PATCH 02/10] Text edits Connector names updates --- create.ps1 | 6 +++--- delete.ps1 | 6 +++--- enable.ps1 | 6 +++--- fieldmapping.json | 2 +- update.ps1 | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/create.ps1 b/create.ps1 index 562c1f5..c04b2f8 100644 --- a/create.ps1 +++ b/create.ps1 @@ -1,5 +1,5 @@ ################################################# -# HelloID-Conn-Prov-Target-Empire-Create +# HelloID-Conn-Prov-Target-EmpireDynamics-Create # PowerShell V2 ################################################# @@ -202,11 +202,11 @@ catch { if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not create or correlate Empire account. Error: $($errorObj.FriendlyMessage)" + $auditMessage = "Could not create or correlate EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" } else { - $auditMessage = "Could not create or correlate Empire account. Error: $($ex.Exception.Message)" + $auditMessage = "Could not create or correlate EmpireDynamics account. Error: $($ex.Exception.Message)" Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" } $outputContext.AuditLogs.Add([PSCustomObject]@{ diff --git a/delete.ps1 b/delete.ps1 index 9820210..f2c716c 100644 --- a/delete.ps1 +++ b/delete.ps1 @@ -1,5 +1,5 @@ ################################################## -# HelloID-Conn-Prov-Target-Empire-Delete +# HelloID-Conn-Prov-Target-EmpireDynamics-Delete # PowerShell V2 ################################################## @@ -104,11 +104,11 @@ catch { if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not delete Empire account. Error: $($errorObj.FriendlyMessage)" + $auditMessage = "Could not delete EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" } else { - $auditMessage = "Could not delete Empire account. Error: $($ex.Exception.Message)" + $auditMessage = "Could not delete EmpireDynamics account. Error: $($ex.Exception.Message)" Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" } $outputContext.AuditLogs.Add([PSCustomObject]@{ diff --git a/enable.ps1 b/enable.ps1 index 01d8028..87dbb9e 100644 --- a/enable.ps1 +++ b/enable.ps1 @@ -1,5 +1,5 @@ ################################################## -# HelloID-Conn-Prov-Target-Authorizationbox-Enable +# HelloID-Conn-Prov-Target-EmpireDynamics-Enable # PowerShell V2 ################################################## @@ -104,11 +104,11 @@ catch { if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not enable Empire account. Error: $($errorObj.FriendlyMessage)" + $auditMessage = "Could not enable EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" } else { - $auditMessage = "Could not enable Empire account. Error: $($ex.Exception.Message)" + $auditMessage = "Could not enable EmpireDynamics account. Error: $($ex.Exception.Message)" Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" } $outputContext.AuditLogs.Add([PSCustomObject]@{ diff --git a/fieldmapping.json b/fieldmapping.json index 806c6b4..2c48fdf 100644 --- a/fieldmapping.json +++ b/fieldmapping.json @@ -33,7 +33,7 @@ "Enable" ], "MappingMode": "Complex", - "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n \\r\\n let domain = 'BBINST\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n \\r\\n let domain = 'DOMAIN\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", "UsedInNotifications": false, "StoreInAccountData": true } diff --git a/update.ps1 b/update.ps1 index 5274fa3..7a075ff 100644 --- a/update.ps1 +++ b/update.ps1 @@ -76,7 +76,7 @@ try { switch ($action) { 'UpdateAccount' { - Write-Information "Updating Authorizationbox account with accountReference: [$($actionContext.References.Account)]" + Write-Information "Updating Nav account with accountReference: [$($actionContext.References.Account)]" if (-not($actionContext.DryRun -eq $true)) { $UpdateAction = Invoke-Command -Session $s -ScriptBlock { @@ -125,11 +125,11 @@ catch { if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not update Empire account. Error: $($errorObj.FriendlyMessage)" + $auditMessage = "Could not update EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" } else { - $auditMessage = "Could not update Empire account. Error: $($ex.Exception.Message)" + $auditMessage = "Could not update EmpireDynamics account. Error: $($ex.Exception.Message)" Write-Warning "Error at Line '$($ex.InvocationInfo.ScriptLineNumber)': $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)" } $outputContext.AuditLogs.Add([PSCustomObject]@{ From 4a0d6e70f239535599bc847327b00f33e2924054 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:35:52 +0100 Subject: [PATCH 03/10] Update readme.md --- readme.md | 146 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 51 deletions(-) diff --git a/readme.md b/readme.md index 2cd23f1..a95c29d 100644 --- a/readme.md +++ b/readme.md @@ -1,99 +1,143 @@ - # HelloID-Conn-Prov-Target-DynamicsEmpire -| :warning: Warning | -|:---------------------------| -| Note that this connector is "a work in progress" and therefore not ready to use in your production environment. | + + + +> [!IMPORTANT] +> This repository contains the connector and configuration code only. The implementer is responsible to acquire the connection details such as username, password, certificate, etc. You might even need to sign a contract or agreement with the supplier before implementing this connector. Please contact the client's application manager to coordinate the connector requirements. -| :information_source: Information | -|:---------------------------| -| This repository contains the connector and configuration code only. The implementer is responsible to acquire the connection details such as username, password, certificate, etc. You might even need to sign a contract or agreement with the supplier before implementing this connector. Please contact the client's application manager to coordinate the connector requirements. | +> [!NOTE] +> This connector must be used in conjunction with the [HelloID-Conn-Prov-Target-Authorizationbox](https://github.com/Tools4everBV/HelloID-Conn-Prov-Target-Authorizationbox) connector. -| :information_source: Information | -|:---------------------------| -| This connector must be used in conjunction with the [HelloID-Conn-Prov-Target-Authorizationbox](https://github.com/Tools4everBV/HelloID-Conn-Prov-Target-Authorizationbox) connector | +

+ +

## Table of contents - [HelloID-Conn-Prov-Target-DynamicsEmpire](#helloid-conn-prov-target-dynamicsempire) - [Table of contents](#table-of-contents) - [Introduction](#introduction) + - [Supported features](#supported--features) - [Getting started](#getting-started) + - [HelloID Icon URL](#helloid-icon-url) + - [Requirements](#requirements) - [Connection settings](#connection-settings) - - [Remarks](#remarks) - - [Dotsourcing of the _`NavAdminTools.ps1`_ file](#dotsourcing-of-the-navadmintoolsps1-file) - - [Setup the connector](#setup-the-connector) + - [Correlation configuration](#correlation-configuration) + - [Field mapping](#field-mapping) + - [Account Reference](#account-reference) + - [Remarks](#remarks) + - [Dotsourcing of the `NavAdminTools.ps1` file](#dotsourcing-of-the-navadmintoolssps1-file) + - [Development resources](#development-resources) + - [API endpoints](#api-endpoints) + - [API documentation](#api-documentation) - [Getting help](#getting-help) - [HelloID docs](#helloid-docs) ## Introduction -_HelloID-Conn-Prov-Target-DynamicsEmpire_ is a _target_ connector. DynamicsEmpire provides a set ot PowerShell functions allowing you to programmatically interact with its data, similar to the PowerShell module for Active Directory. +_HelloID-Conn-Prov-Target-DynamicsEmpire_ is a target connector. DynamicsEmpire provides a set of PowerShell functions allowing you to programmatically interact with its data. + +## Supported features + +The following features are available: -The following lifecycle events are available: +| Feature | Supported | Actions | Remarks | +| ----------------------------------------- | --------- | --------------------------------------- | ----------------- | +| **Account Lifecycle** | ✅ | Create, Enable, Update, Delete | Deletion is currently only disabling the user | +| **Permissions** | ❌ | - | | +| **Resources** | ❌ | - | | +| **Entitlement Import: Accounts** | ❌ | - | | +| **Entitlement Import: Permissions** | ❌ | - | | +| **Governance Reconciliation Resolutions** | ❌ | - | | -| Event | Description | Notes | -|--- |--- |--- | -| create.ps1 | Create and/or correlate an Account | - | -| enable.ps1 | Enable the Account | - | -| update.ps1 | Update the Account | - | -| delete.ps1 | Delete (Disable) the Account | - | + +Governance Reconciliation Resolutions are not yet available. ## Getting started +### HelloID Icon URL +URL of the icon used for the HelloID Provisioning target system. +``` +https://raw.githubusercontent.com/Tools4everBV/HelloID-Conn-Prov-Target-ActiveDirectory/refs/heads/main/Icon.png +``` + +### Requirements + +- Service account for the HelloID Agent with sufficient rights within Empire to create, update, disable, and delete accounts. +- Service account for the HelloID Agent must be able to connect to the instance where Empire is installed. +- Linked usage with AuthorizationBox connector + ### Connection settings -The following settings are required to connect to the API. +The following settings are required to connect to DynamicsEmpire. + +| Setting | Description | Mandatory | +| --------------------- | -------------------------------------------------------- | --------- | +| PSModulePath | Full path to the `NavAdminTools.ps1` file | Yes | +| ApplicationServer | Name of the Dynamics NAV application server | Yes | +| NavServerInstanceName | Name of the Dynamics NAV server instance | Yes | +| Tenant | ID of the Dynamics NAV tenant | Yes | + +### Correlation configuration -| Setting | Description | Mandatory | -| ------------ | ----------- | ----------- | -| PSModulePath | The full path to the NavAdminToolsFile *.ps1 file| Yes -| Applicationserver | The name of the Dynamics Nav application server | Yes | -| NavServerInstanceName | The name of the Dynamics Nav server instance | Yes | -| Tenant | The ID of the Dynamics Nav tenant | Yes +The correlation configuration specifies which properties will be used to match an existing Empire account to a person in HelloID. -#### Correlation configuration +| Setting | Value | +| ------------------------- | ---------- | +| Enable correlation | `True` | +| Person correlation field | `sAMAccountname` | +| Account correlation field | `Username` | -The correlation configuration is used to specify which properties will be used to match an existing account within _Empire_ to a person in _HelloID_. +> [!TIP] +> _For more information on correlation, please refer to our correlation [documentation](https://docs.helloid.com/en/provisioning/target-systems/powershell-v2-target-systems/correlation.html) pages_. -To properly setup the correlation: +### Field mapping -1. Open the `Correlation` tab. +The field mapping can be imported by using the _fieldMapping.json_ file. -2. Specify the following configuration: +### Account Reference - | Setting | Value | - | ------------------------- | ------------ | - | Enable correlation | `True` | - | Person correlation field | `` | - | Account correlation field | `Username` | +The account reference is populated with the property `userSecurityID, FullName & Username` from Empire. +It is advised not to change the reference after creation or correlation due to issues in Authorizationbox when changing those fields. +## Remarks -### Remarks -- Service user that runs the agent must have sufficient rights within Empire to create/update/delete accounts. +- Service user that runs the agent must have sufficient rights within Empire to create, update, and delete accounts. - This connector is usually linked to the AuthorizationBox connector, as Security ID is required there. -#### Dotsourcing of the _`NavAdminTools.ps1`_ file +### Dotsourcing of the `NavAdminTools.ps1` file -The `NavAdminTools.ps1` file is dotsourced in all lifecycle actions. Meaning that the functions defined within the _*.ps1_ file are imported into the current session. The original code was based on a _PSSession_ with `Import-Module` and `Invoke-Command`. Since the _*.ps1_ file is not a module, this seems to be unnecessary but requires further testing. +The `NavAdminTools.ps1` file is dotsourced in all lifecycle actions. Functions defined within the file are imported into the current session. The original code was based on a PSSession with `Import-Module` and `Invoke-Command`. Since the file is not a module, this seems unnecessary but requires further testing. ```powershell - # Import module - $Import = Invoke-Command -Session $s -ScriptBlock { - param($ImportPath) - Import-Module $ImportPath -ErrorAction Stop - } -ArgumentList $PSPath +# Import module +$Import = Invoke-Command -Session $s -ScriptBlock { + param($ImportPath) + Import-Module $ImportPath -ErrorAction Stop +} -ArgumentList $PSPath ``` -## Setup the connector +## Development resources -> _How to setup the connector in HelloID._ Are special settings required. Like the _primary manager_ settings for a source connector. +### API endpoints + +This connector interacts with DynamicsEmpire via PowerShell functions from `NavAdminTools.ps1` rather than REST endpoints. + +### API documentation + + ## Getting help -> _For more information on how to configure a HelloID PowerShell connector, please refer to our [documentation](https://docs.helloid.com/hc/en-us/articles/360012558020-Configure-a-custom-PowerShell-target-system) pages_ +> [!TIP] +> For more information on configuring HelloID PowerShell connectors, refer to our documentation: https://docs.helloid.com/en/provisioning/target-systems/powershell-v2-target-systems.html + -> _If you need help, feel free to ask questions on our [forum](https://forum.helloid.com)_ ## HelloID docs From b3f0f8948c4dc53539578c96e0e9f7d9d978e63e Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:57:26 +0100 Subject: [PATCH 04/10] Textual fixes Readme + variable names fixed --- Icon.png | Bin 0 -> 21248 bytes Logo.png | Bin 0 -> 21248 bytes configuration.json | 10 +++++----- create.ps1 | 23 ++++++++++++----------- delete.ps1 | 2 +- enable.ps1 | 2 +- fieldmapping.json | 27 +++++++++++---------------- update.ps1 | 5 +++-- 8 files changed, 33 insertions(+), 36 deletions(-) create mode 100644 Icon.png create mode 100644 Logo.png diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dd07cb7ca7c072807d5607825e08ccb0ca51d7ca GIT binary patch literal 21248 zcmeEtp zPuwr=7khE-UWb`8b7sEtsq;-!T@mjEkuFGqbJ1ys}>6r1kl{RwslTk8< z>PzORG;|pr`fW{-<1)WhT?Hd=_IN53tqfo1W^_UYlHM5b{zpGQ`)Bg<^4tT9*Gls8 z7qaAZ^70$c(Zauer8W;GBuZe!C1hZ54@Z@imyhL<{`z%*2=h4+kw_N`Jp+T{|6TmQ za5lsRCa*Jhl5{I{v}!K3!Z_o~ZtU?);;@q8VY!FkgXXz`iTu5Zr(z(v9ZsGQr~(VyBYcO27x*8>c{4+*Z?YKd zyA0O#IKHh3{Xe19#LRXGyYeO66V30hKx-zhSY7IL7#vq6zsI5S?Yk~2D{g>TzK__E}eI45~V&c%t;g`H)yt*(&tIABHNg+(INEgLTv(7qaSXcE&93JT`iB zo$|A$rK1qKve<_NKu9Z^G)-FUepKuA?Y1*uWn8n*O->=m+;buFMX>z1!)jIWa#?mrFiQ?>H ze2K8HU7K0Hn}Y26Z#so7P$Qqy7`NUHdc7@TtK|B00=eM;xxCV3)7M-sKhZpc@2PPf zk`61gE4B@3s{oACZ^E=dPmE-Ka;O zhvTq1ZkirB(Akp$9mK?_VGE`3HP)po)-$cqqt?)R#;-6!W1+1v4ytywQ*Crm;{32k zV4`!`M~Jz50@0b_ zin?(BgLP4Y5*UU~sZMn!*V?Y$N~?N)7*Gb^bDGw;edt2mYU5MQ+e~|96coD{(#Pw4 z#T#K7Hh5o46^&1d`CI@R1>^U`@di6dW=wR=TN=`i%Io*+TgMEiG3eDY>uez9u`tlW z(|^nz3DsByGOwMwwuHBWBb+)iFG1Y$CUH*s@dGKpvsoF-(e7hPzM6R9656YjT42Ca z_^!Qq8D+G8ls)1c{JMKPs9H8J@CoNt#dg$|S1pn3|1=w${rcLKF>aWTwPMTBBiOIj z3;gh{SOM?VO+1%g0{h`wGMoH+U_Rg5@ap!=7U6=JRo^5RN!Motc>ml|zys-&nbZm; z$3_#_9pVd0 z?$DV77buakUt3E?h&fAB34v_mgHJ0 z?FF)G_LJuz;SuPO@DF^7OOZjz*e~__^#`LN|XM z&USS}#b4ZsR6k`(>7}Eu{arvGghX^@?c9HoS zTh*LPSEec7L2{{%ra2M<6Ee$-TAQQuN1(=rOne4bU5a|QC97@^9LH|SWHe7R*t-w1 z==<-~f8tf0D~vCE#o+g;SAN)MR9WsUZ}76o*uOp%(p_yR2*kDDtZfoUMxR5bDApys zMEdUUcmoLyM=N5th)v(YI`7}?CN=4l)!zlD&HUM|p^BRm|M&X^jE-w{kHWV`e{XZc zAV8xr-eAs>sB3%4Z>2G<*d#*U;N8A&psI-E-vl-R-KjgcdCY%0Jw$!U=`N-UqXa9t zi`;BD;|>F{XH`f*gqI)0&!noxD}<$HD0_<5^e%deE;}bZL_>kgwO_Ief?T%T{3K&G zJt;tgc>0H4v;R(g-y>%=RS8}2dU+;a@gnHtv51ZX7d#fc>T<#Q9?B!dLvQv@VO&&5 zP3tOfRm5C8nazKNmRwPM!$Dlt^8|Pd<4)VdjZK-Imzx|@XMWI+aU~DD&CN?^vMh#K zJGg7>n$&H)m3O&eF{rWuDsd%Q#=~nvfs_vODnS!V5n%m=#}mJx;{ClJ;r#|aFHyM> zVfccW&kre<-<8Q`!0;OVp&6_cUmcJxs zrH?hD)P({cE?G;lGpWuzA!N!*sOh)VAe|Y_F9~MhFCcMo)QR-pU>>1TmbT zICY34B@zIpO26pEt0j|3))oP&3&0;P>gnnU*B?R@AS=@~wc6lC%t*^yrrK z;hRJbaST(GGH*yD~i`~3^SV4c^RBQC`{8VoG+N|EKuGh)5Pzy_Mv~Nl1+8K5)3orAa@isPH0YRkC68_M3 z7g*+^Ekl|oePCo+VKiF~kMWpPceA!3|4mAvs-R?0YfMPGx*2^@ZKdaA?wuj6iJc$% z1FD)Q#;IwXPii&6DwiiXDcY(~XezZy0#edo%_}7m2aRw_tWnSz7Vv0u#E!jf(L&xq zpY*#s&t@B98}82*EMzXeds{Q2=QgsE)g3lWaC&|Ffk|I` zpv2=%%Fw0M`GM%e#HRhmm!saIXEIU@P%RiiJz}DXC^OIpSB?cjWQU!ROYi>?m{-PT zt##x4&?om064Q~Qgv4YeUHj?E7AS0gXj^F8KahvAzp77^cY!qA2dmEiFlxczZ_f;E zvFp=0D~F_kj5{B*hLYaQ1$E+W6=}|C0 z2iOxd^J7V;i~Wn&qX?$04+_&~t_T>oY8;Zg3YlIuzekpcx@jYu7dizGliV?IK14S@ zEQ&SWwJ1A|Ii*!8FFL}zEvOvzvzZ8J)Nk(tf3Z7uqvRGz=GF&b zUbIW{TZ6&b23EJtFEUv*>=CD~swh%a-w?KnpKR8&ew;LRd@X-E7BGJ3-!}+`eQ|HX zZK-eh`NGqbHz+ZhnL~pcu_v|kr{T%7J5UObJ-@9FgR{I%A*CX0;E zW(}np^Ot!N8|68tlYie#vVzB}uRQTq@4Fis9|pmNdfJy&$CHNShtH<<-WO?iUabo{ zKkm~kT{kQmWPpVb-0}h6P-SB)tW>Y9d1Eq$MGC$4N^xwAl><|gA**Pa2QM-(!j>nq zpvZ~crUuyJ-p>G~O6K>j!W7%STDL(L@s%6y&l*C4aC{`&qBs1b=w$-{h{?^NKgTCo zbN$Ycc2VIlIYD+R39N5YJ7)o73>AV{2+vV0PbMypcwlu>E}P$<6|e!?o7l>|4M<&bp7Hz{D52C5?BY z5ss!W%_W>C^6ZPEZd%T*{{&iEFOU3Lh}*YN{i?%OPZ5@Oe=n&A@(>Td!bc0w&LI8j zkoLBctMESrl`n$m9#M9>$8Lz%i-gN6JwtfU3rX#| z9eLMw;`{}tfHU_8Om;4(!t&}WbACDH|F`D2HQXqk@h1$*5T%_4onw2irP)r7Einv9 zcAelL4-6=LwLB?}Xd)}&%M{!ebVI-!s(I>|IBT{R#I?iTrVM0+syY=?nKCa(;1b>{ z1L*EQ5Zv^m+gi^jno~$u<|a`5cdI0yl@Dw-t^tCuZn?^G)cvubWPV0Kz!PAJ?^n9E zFJM=!ISVh~ESyafaO84UjxKCRl1@6Chfc(?>Dl|j()!8xlsRnJf0uJ*)s)MuUFLIx z{`7W|t6%nx(lvkR4V*lv2)Og{NidjqZa&ex^@r&uu8gH#vPP)oo#IX&nT1XeOug&- zGE-4CQo`G&E8pok*~l%k;P5t+aW9L zIm2^2Z7Cd>d*yd%k<+JXk@JB3WdQlmCca5@3t+aXw;fL^5}umI0RO$6_^3~x6H#5s z!-e0=^OrKQO60g%rQ-F-#b3o$NnR{@fgqpk`@e59C|=G#%L4;N=UBJip|Prblx*5y zEOFFC)p?1uyFw6Yxbw}hQJ64=aDLwY{73j6!a>9B3Hd<@x~E6aZBzauYa(>|s61*c zm~KG;jnVsdtQV^8D z$xr^7G$%RUVHluMHsuWa5d*n zlsu^q9Qw?{ZyW1>bo*DfS9ZprZBv4hH+!W=Ai%*mN*F7?TK_&pqRldIa8uQ)F^+PaRUY4Z|IOpq-R3AP zpZbNs$H!KYRZAypH2?3%CRxYTizAZ_?h?=FX)9J*OkdAHV(xw_@w*I(e0v;Ig6 zqHD?W(ruXKGr2)3E-<{7|LRSG7On=LEr{XZ8ZY`CUpLCZQM=V`5No>v>C8vk(q{@fQWwW%-1#4>!9Mp%;*QZ>KCHE+3$Bby_|(Cs;n}BHp{P{lOq-kq1k@iB}w^OZpv?M_4fQ>QRu7 zR2G)ut4O0M`pQefs88seLVSTMPkXl4`Y5xe+(FVa{b+uLaO_gihksvX{4IQ<5jh{k zq0Tu-wznMUTmUcHD$Ofuz<`X=%ECD3d?RgWRVnH03iST!6qXD@-qsURqXO5*~q zi!R-MF^BpnE=a?#=?vyqQ+e1@&QPj~K#s_q$Lw{Q?*_9?h0j>yJK;-3Sx=8{NXMo`^&+HbLB` zoRRR?+BTwA-kGRI^-gysn{24~$8Jur)1xQaY>o5Jow0da6T37od>EpqsDNBV0^Kwo z=eeB=Fxb=>M{FUzw*c8tEdG?CJ6l;{6~KvJSeJ*k<)Z3)(R;7$E12zR=3s~O!BlN+ zut7s>gw&Vef6n(B+w%UC_ZsGvNa{<^ZbpFC8}*&S>_j+rI<)n$?%z z3CQMZn8gE=BRYQ(xLhc@%F6a^dwH`v2-e0t5I${<_mv83tZvg>x!ln; z5QT*Hd%=^u6bCJ`BfqU1MTE{XdDb}Jj1*ULo@-7&qs6Ta;tlp|{fZ?HNL0OP?Qil? zC9>;Ju}tE@)x1(op@XX$<~NfyX=?u#D%r91z;2SKE5fm@-sGs>;7O2WC^ezK_~C3X z|53%37OW@|`oViySYmcxPUl76G?)lDbS+)6YyxfB8LNNr?@MGW;q-9Xreuq?RC>jHt#54{3mL$Nw~}&vpn7Vvh4VY<}B6-TXV*>E_?LAJ5B%lcO~~ z>fP^VSc-vkIb3FYXZ#=&zwnE93$tr$DVm33lMucR;h2@~lEsG_yJJ(KfTf#9dNxUto8 z6<%Q6TSNQP2sH8?e*e4!PpbB|9KI?7>uVIwx>?bO)fvC4he8R2ToQM>Z`yg(k@7w; zEsG+Z&if5<=Z7QwdSF2{aXACKdl-Wgd#+thLipw}e7>fqBP=3OUY$qUL<^^sp*>v# zab!;_!piTpB*%WZ8npBLOC`-jhBWQ%UB^A&ol3uixJmd%MAm-vzjaskg)T96Ca=0C zkcL=*!(5V0;2SZP+FZY{(k(YGB;~$F_Bdc;NzD_D z!*BNqN4mfr4 zvXbNZ$>ZH0*t(Mcg@!k?Fv)g zm0ju%^6$QdKHBj9d7kbwL|6j(x{S=aa1A7Gi(< z6j}s?gtW3U!-LN2!~6Ngjf{RfQq@bc^mfr6e?AUg(d-EERzyPyRyG}(V0#&`IYZ0t(QWG8uRC7DPw$F2NIgUfhy4;Su&d8S|K&In zIYq#fzV>Y9K}mwFg^U=Ir9vyeW^&0Gv<7RHJ)rCO%o3vOrOzm^*DtLlYcVDL#6cjieQ_d(B> zvfn%ZhV`x?yPX}vkOvgkT;MM*j=0&rYARGVQi}E_j;8I;=4us;!RmTG(hZw&|HdfWZecGuoc{CvG^m;YeVNAn>i1cNIC^|fHuAh-{l0)tqI?)QgOov;Gy~y=z~?!}_wN z7RZ842LxFe{uL%a6hqo?GU5E*2}3`6>MY>oPoU{tS-2j9mIj4CGmo^f%4%478nL9Q z+WMIaW$6zX`oXRJd5VmUrJW;F5p#_b+57UIm%iNhFJO^Q^u#9!H5XhL+`3$&xDqGv zyL?ZFAEQ-VFXG7qC=|yb#Gq%Z^Vgagd-|JdwO5k@U)Uk zagmIk3r$l@WHt9rH$Z~hq&}~7x5e0)7<*ZwC9&)OYm%~SuHGxrYHtltFcKA2I?hT< zAiN``gUH0qp9%n2a;SjcvKgeiXO7I!ElB z5hui!9)7cG%Y3wL&NF#NX^&ik+Js5n zw+mvdx6@8TXTHfRQ&^&C_y9QC2NrRn5-VOLeYH)oDW3e*yNKyr#_Pg2%;TkS?6 zGh0EUu$&i!RpSI5{av=@v{F<~-Dav2=m{U1K8<-ANj%>2PECn=ocj|jCkO4!(3#wo z*IJkV@K-(Toz>IRY6-@hD^UDJ`l9%^5l9*^kaYhI4PA0cIuAG)P_NWVGmuqiGc`q{=itTrz(PclDAL0cq? zU5nguyK88uZcF5;1OMl!yqeZu_GrXnk;?otkVfP&*F*v+4P#D!;Z>?o#@WW#cl#gN zdsw8n#r<&uoX2oaegX!$vFz%7?Kg4BwB?@5OWd=CdzXAv3%m%M4??Ojn$yXH9R;<~ z9JCg}1Y=k4PW#Mo3~8>8soc%ym^F`sy$PZ}48I|bsyu9-IxRF%L|5bnbzU2>5PCQ3 zep{m2+=cwE0xLRsy?RyZ+j`&@N1IDX8)JEgf!IPhxGF3sI~VyQ$+<+ zOKn3192{#YE|22pO~hzD7#C`B`|Pau-jbi29Oqlt(yMc4*3}n5%~pYJ15*!%Px3aT zH$jrPJ%Nq9&saWA#b+Mv7jUjiqXrv(u_I;nWzfbh@!Z*_lQ?a8w&V zYY?ma0QU1XL*SvBXsZ)z7$SudF%Ou&UJ?^e&!S^)O;>ijoU@M`Jl zUUKW0Qz=!+vB~wHaaFwclY*E&;+up@?B|A*4v{8oMT-el`V-7fo#;FQlUs@z&A86pd{ zRfJqIWFNntAqD5Bp~l+E@iAf&9tTiSFhE7S8DHX=U)=vfA3zty$^sEGbH-faC3ZAh z$jWQv@#9>lCt^1klg{cq)0Nnb|DR4h3wm5qK3qGRQ{hnf{j~c$u`wD1{kX?IlRQwH zQicG0*e+@DP#=Q2H6=yzM;hF~mrOjMpgZ*Gl#*(|O?hOfSdtYAo~$feVO(xJfp7D; z=F=vR&&OKK$`dCUHH5YxLq82U+Ws9G$qlX!H)KO2K*X+rkK2RqXC=U9b1{6>S;ecX4OIR3e$MYt9*U zT0gU=%&;D+Y;7~vE6_1vea>)}YKybhCg-%{F#i!Q&y~>Kwz}HgT^^UPGQ{7$Ckcql zbku()N>MW(dMP~dRq}Q<1rm5T3C1^=>~|AI@TQzN4m*C#DMCBQR#rgu(#GFWYsSy4 zsE~8D*O!6M_)29ZgpL2+hU!DcB*G3dc< zn~J$?!=9L+QoO61oj_|u3Az{qmrv!BWI(0%Lp&b~48(MLI2GMJexqkg3UnKZ69;z>aaDv6^AXVJdxSq zAZ7v(!jD_L=vmpP^hr?*Q^W@@Re4t@L}kp&%`1R|l2dAXt+{&~hP;HE~nmTKJy$gZYRBiw+Ljv~4gtzEybjgEa<42x+z{&N=i&qgGKRs41f zTSdeNL|Amu4ETWktD;fWIW$6IN1|3&4L_2xjx*oL_~-hh=xKnBuA= z0D%ugU#BOPJr@JpSJ7WbQXlp0#;NB$$#e@0AalI5pRg^oF(T8PtL+~9uQFwdzrq{tWD7lQyTC?o17C@&C)kEE?aGm(-d zVE%lDyk_yXC^3LW_Lf>rCv5^g@x_u4*eQ6g`=_v{_sfLF?)3U4ve~3piQJ~2eXR8e zWMz7jrc?Lf=#Y2+0``f;<>W-;8UW-+gSNpu`Qp;OIox#Iaed((>3kGSyigNJl5Hwz zf)Gyh_oI30)b@C3hpL$q$?q&U&HE%Sz7t3|O;k3W9FXKiSiJ#t?L=s0-dw+2@c&N{ zXk4i0B5%bz-e6+e^_KuSWr2L!+XrHP`s>>BPWL zfBn@}Qz=K}WUraztO448`bn$RY|9o{7v$&Se$OGR#^0 zv$2ij4F~}B8q0KbM{vFcs~5##CP$>d84JL>d_rQo%{}%SGS&zYSiUW@87i z{rd8JWVpA-oan;m^KG*%YKUTkbBR7a+9!ftst_^LzRe85dUK7mR|HhabR)D|LsK}MXm%+3LTfTVL++N>l1@Fa8SN&xejKgT7=(-n%eL;Qn zdDB%eUBukg{)GTG;c+p#@~pQVJjpo2?0NQwlC^+C0pFHps_}{PNYomL3gC=Y6rW_w zbBe1g<1N0#qB#p`#Y~! z!EjeB^?eH3?RRSg!Ux3(Q4tbSaSNAJQ=fv^$OgTw;7O)zuRuApbqVGMF5oW$Z|&J= zFYMPfyyJEz7W4AE%Y}T=%akWL?NEhsyeWe8wZ9Iuq#S3C z%H>bMdg^?AmI>fLN&I-Db25KV+HN?=kk>M}mk1`Cd$U$;_R)ko7Hh=*9C2tqt+1xU zmDc^hP5bXuNF!l>EMMDeT6rZTe~kziYeH($#gG;(m-U}$0M!g#-f1p_R;D?#3&sA} z{UXzo|679^a(TOd0u#;YQ#<;DGLP027309p8u&Kj&j(hX_YFzx-EtYUpbN$O>rGoR zuB(RJ(Y24HfEFgzBoZc;v(&pct*eg-dFw<_g+)V5x&3s@f!Po2T}bVuXQrhc_H0!p z%K3RQ`*4zjI87J2X6#u6C^Oe+bVQns)|u&w?qkkWDvamQkh(=*dIZe1`ow1O@}$ph|$V@<`k z0_T!`IXS^K$TPDA7h(FIKO%;AB+TpCcXvN;ABY%`i0(&i2U4R0UDhX*|Eq06Z#Nld zDE;6)YNGF4u4Rw6__lml!%O}|A0q0e^Fo=1&Wo)st?OZ!;N*Jm!v>R?0~eU*SA>S=0SK7w6 z@JM?nI4AmgF@G~1ceWHTtt9*5%<<;3jT`mt0&$OGH`KSb3WEq@^YNE^-uE;k47Tn3v+|sjFCQJtQlK=M9{>F1u?ch2UgoU-8Y>lAkyf zo8YFXe;O2Y{^mc|MlnP&DCHxTeGvm2F?F2-d~N_2_Q4M$Pdb- zs#W=!RJ2him06WbeH~v78w`_GXR@dEH>11c>%23}W*^95AMTDpreyeSd^wLgj?cDv z9FK#a=9IvBo0y?-HL4Bl3C||C&k;)Ep--DSw8365szb3VJu+n^li_GCrEv(LM zgQw!@0(p`pR?9ASxR*3R`mWtkRXFiC6jiHq=$QS@V1Np{%?}ddZ2@$Sn$O=S z9@(AI=IBn~Qg^Z;qHE^aQBW|g>NLCd@LYu35&N6E%e)T57Ne2I{+%?U{&L8b$j zjtHAudU1cVM4WCptbxlho&c~NeD>6`g%~7?=A>%)ir^=O@v7PYW6WI|L-vMBD!a~a zgEyACM)bYJdPP(9#ua8V!B70AeLD4_8Q=Jw9f4rA-V4>U0H>zk!L1GCbYi(e+ZN4V zSI3+Y&E8<;_k*KgegFJ_4q&Z&i$XWwuen=Su++kaMe%r`&sa#y{7>&s^HEHSWepaqTH` zlJ)MU9r1mLDVE-UIu$nR%3l)7i1-0J86HsG|BtVVUeUwfuh`t)<%_#dl}$_4a)o%m zGi>{iChn}9s`gsjV34e6vx{(^9XFolZ!+aX3b2ITMIyXPG5kEsi2=7=K-?4bG+EE{ zc;(8X?%hkc(0K}*|AaqnD$&zD95+Hi>#uA)@5PL2fx&SRhxc!ZdpyAFow0uJdWm=;VrEAi~|`dt#BRX;N+@cV1rSDNj~+GmPy; z^B?W(hGAyA*_06u0i*sXCY}*bn;_fVor5}UY%9Cb-D7rlWs=Po)^9PEe%_r_qED&4 zwt3bUkP~Di ze5A5^wz~A%>Ua9)ATV)z?ddsA74>#0Kw+4Hs%DlsIpw>bzqUuRnA_BD`C2ycE&|yV zdmA#>E8IX!OX)v1xDtntv!q?0M5FJBsmrzLr{}&nss2^r(P8FMBb)I^229)Oc;2T~ z!&e_~?rb)XI~bt}EW51~8g7;;=6sf+!u&r+GQi$0E?WuQyMS7vKA$8@zfrn&xk4Vy z3XN+w(1gfQPXlqBV5x8Oiyi|uXIx+>ljHUm4Yu%EB0gJ?OB_7M^A87kX8OC!JpqHk zL{>tukM2e>PyckJ>`A5w+J2W`Ztso2W^DK)65MoDQulAK z-o20pL8G<(!CS$fJ#5znWN~)R`ibPDzPHh=-#;7!06&wLNM-&4Z1;;S24sW;pF{D1 zKN;x=t^}E$K(3SaFl{LEua-eo4#daTp#5J(#s?!)N)WP0+VqDWq<*WePAf?z_59y8 zGP_QRU+D)i({x(qF2f>MQlZ84wH%BQH=azV#sLWZy~UE}{3Ia0q3&t9-@Pw@MT@5t zjp)wPGd^%xoCK;C!t(Km`n6O>*VcWqF>y+zV2WY=`0>w)ojN=v++yMnmNc6BCqnlpzJ z=Rw8nDdWa*ASr_=&GonNfe+XT6=`|1*L)k~Xd4(ixbceGwU5iP&wPYDtk;O`ohvl_ z)o4Rh|3HwKhNqpbV*XwQJ$<#@D%}=N*2^&Q3*QBI8GBzYYNTefnWm5;b%+Z!%Zlh{ zTX757-fv~-?-afi_&-p#0#J%HIc9rFzNL~d=GpdGm&XxxYt~qOCERJxYi*U0wTEo< z;Ef%9qH2JY_ce$7#vN19vumApWS}7A2UP=w{af7l?ZUc(7+H!Oz&IH$dZ{gXv^G`A zPQy=d-6}i_>KY%l_teC>&`0oJA^C8vgs7Ptubr zV{eH%7a~78GKa%URzCn-B19cpo&&4J+km{M_kg~&{?>?

MOoq?esG1eQBgwk*7#=ACgaKETf+=K+6=Rm#&@UWtSP zuLn`SAq9)0|A3e%y+Ku`C%8ocJU6~ zM_(mOM|Ax61Vwf-o|GBlLV}yWl|nOTeoJV-Yy7>hf`iV=)WRs!Dm4>H5V<(%vBeK-B7(hq&NzWgZ(wIF&Nz{0j1%B?AY*r>hQ+M zqoHGja6gJ4zL6i8C$;Nv^b5sTJ|Nz*;*mTK6IAyJKLxPe1TD_t4(0@-9W4+Gn$PeU zXl&jek_A#mJ55@dcQ+rgf6!D2!n@r3FgDBeKPSDKR+()NX_$12E7K8lwS5T!UP|;2 zc#qM8opoAhl*nOy_C|%d8zz2rn!baCFUL)L;|a+ThX$(Ut5?{ zIW+ZEu#=G{E}Ts?Jgv>$R+RtDngmRad2P+gaTg${gyzWYlu^}GyRTCD**G=38!Y@^ zm9uR7?d|^3;dS&vH{{QSoJUtKpfMej)ULNTU}eL{=y?S3H70Jb!cD(I~MhfMDW~d$z=fG~wvi#t?BLyR z;>F^cwfdxSL%JP9It_kI)voRmu)df9&#$DY#FrCP%|rKbri&rBwUhHed;7eg|gW`dB!)4QIYhRoxmnUIv6D z6l?|AE#vbK#~QhHHa3dYdr{{L_u>craau`V1|xLw&X19|Xum*`=$TRkRb$Oll^hUz z0fHaQ;4pJl1Jz-5cC%5!GD^C$H$!)#TBUTg8kTRJ`%!L){7Uo0lb)Te>5*>M#0E6} zPG)ck&e`le)dTWvcq}c5t%~!k3577K=nNqHk85O~ED#6ig@8OwF1>ncq6Tcmf4&`Q z5p^VW(8JrAfEd^+E`&Hc`_l*dQu<@N_=7nd219I?A{^3`X zIWfB`KckY4(vsF3Ie>Sbr7R?jdJu==dVCnu+uRM{2keH4N_N&aEn%d=ybG}nn1+M| z2e}~~GFapKITei6>HI+pl`R}+syM^RmE`Ino-Y)+sNb1kQ87~*(mABF9jGu!p)LP?3#)k02btmBkd_6VI1SwSPU+=!7 zo}s}aYZACEnajn(k6_(4A+{+p;yc}oXi#5$P>|L(THb2X{E(6_ljcy;lD^6dr=XpR za)b*__qEu6uqJl!A>TpD-pI)Mb@G0fT;(^GmjG#cQxQ#Oeu+$bF`#biS21mMuC25` z*0+W8-qUJ?J>h95GMbXX*|@xR6_^WYmm>MWK0W31`m?RK#yWD7GUuVIkZ)r=!Cr+V zETZ|BoUGm|Trz@b3f6pg{WX8yBWR2F+5X9`o=Vo5{l1F_YVYIsncoiI(923;O&Uux zK~{jMG`F*h=~-3I2mu1BdlYTlo-M=@1#&j1f{WkzA1LOh*ruEXvskUKHr>bLpsYO7 z?U0pg|JUc%^9>m+N=h80}k?G&>Y02$nfn@OQqv-M>+c|)IitiOEzco#jLhSj0=%0gJ4 zM`J2EKYBBr{pshs~(YRZ@OUCz=KbP)B|Pk2K2ECTmhG4DL~Hd}zC_D^XVfhwia^|@LKnb#AIdc}E-kKS2!Vx2nbn=u zGVR4YGCMnn4uJK?VLX2Edt#6P*y?dX#DFFCcqhE`c=jMgHrQ9d&vqioLHF;O?NEh- zHg5cJ9)`4neS%@a%-t$Y@8=WWQOr>P`kQ_^XyXKl;y^!b^;q-S-TjW(RCFzEPFKP? zWTdhMD-E-Z8D2yg`{#V&<2AE%EARGQ$OGYx;OKj{$XnvNsiJ@oWuuBmFMlo$a64rD zER`$wDtFv5pb{+g>qv{?GAlyO z6M}5VG+@5X?3REhUSK)RjVv%qdPY9DTiVfAH0bA~N;!V;w&P{rXR{!2^OP1`pT$}d%*koA z)eJy4y7)Ddeg)KHajfS3=DTI}tq6!J#WE z0HqcUOXlzVf9;(2KO6iO$J<(^MO$k15qq>m>za+&o7#J()z*koEo!7<7gh48Rimh_ zw#1BZjS8iRSy8L@4o!p@xq0rNabNfM@2}tUI_JDU=N;IW^0s7#g(1w!2#MBpG+YO1 zADJD}WkI)_8k<=&+7GhpM<5Wy5r%2i@p?XQZw0$q7Jm>qjWc<^$(BQy;_ua` zl#$gSC7^m9M%ZGG)@P%OIDjhfS2t!AyvNvFSpq;G(g^c*?`Kd-QZG z`Wr{<`o&GvbX%4H1^#MWND0G(DL~f(V6Rv9WTW}$!smuJBD)CEDCWi$uP~!x=JYJo zCloh=kZDU zROGd=voJTgrY_FIRtPJ;gNZACtf?HM5;q&XN>9LQn4rMlJqJ$-9YWQJygZ633X3Z6O@WDu-_C)S{F0xur?j<${A8AJ z;!8JI1A_E}C*d(up8|yDvxX(34qLNi=joL?+po&aN&rxP^-Ay9#WdPPA~Wrr2b8w> zwtD+FpB(pC`j4mEn^@X!wdhOo_0~_bFHbN@;PB&=G1qJfKy*+5=T!ey>AUh6ld-az zNfw~AH^|10`#L~f=toMX-EuP1*#-+^F@rTV%nGF?+AnXd60IrcvOq75dEnZZKPtuW zCw5->UE%B0fgJOf?O_tNLXr%tGs#!gG6>4^T(S!DnTA1iszkhGo1Th^R534f7XPIT zS;g2#d36}g>_3I|tyT=mVC3;AlBy-(c9kTeQtzYm21Y$Ug3(P)Q=k) zp+S-nHRg34!*2Nt+NIlefD1>~^QBAp)xcfq+_vdI9p#+XQ+^9)K2PnqiKcB6LSO2s zW=ChXtUa)XjDchNd-kghHFI)B2+k1r}~p2Oz_bW^SZj;lY8^V;*8 zOX_yu%*YzJ0gZRk(EMn4J3BnY1b^k{eMdoEM`0eA)5x)M2buOF-pui7Pc#8@0otiC zl^gWvbQB`NCKI7SO6{QMWP;E*sZ~JuFpNo9Gr-tep%`czvcrS{fDXX-(rZ#Y>31Uq zuXXV+Hp1AReW%Onz(J zWuyK&EEQp29kN@`Qf}xCUtb!ceJof@^TE&s0fp)gx&Xgj9~KrADn#bH9tGm|OPvCO zLHMQCW5%~0!xLpZ`yAB=K?aFCe$p=(CrG4_>dZUB6c=DVWL{g17QhZlT983devGrq%)yJ7(K6Y_#0nwbs+k^>$W$}v(0DHrNm-sgB0T+x$R zM!bpC%JaPQf(2vr*HFaK;x9=4H^PACF&S2*a}pAQMh^|Z8xNT6QzWlAep1St_vW(= zeYx>Hcw}>&&`II~ySOBIkP)Vv{ArhX*%)qSe*Zh!41-Ewr15*VXc?}Zv`aWa?^KQ1 zks6d;B&jo@Z=EPYq|KKmj;m`@KW(a3rTS-*Y<{-gop4QgSp{UzkHPA|=);4`>C`jz z<7!O2qH-V*ZF4BKO|jUrDaF~OFb$}XH|N9$P-8xvf2yiXRq}Xsw2buJ{HTTGWnlmc zE(6PByUX3>2wNeWq3-eE#!PQ=V`(k=duXA>;*P_y8s)~+F%S46QyoCe-0E7BJvH6j z4CkGFw_CsUGG7(W?qGQtDO%Td1^yS}T?y%?rk_9`8$^M&_9$q~tT6}+3@vf|X8R?| zy26cOLtbGm2!B~L5Y{FVMGY?I2oEg37KU5<`p^q#Nhc~^Ej%ZGADOPJmagQ&2-Bpx z=3|*UIT_ENhAC4k{gugsNcY{i8PPfiPe^BSbWG1$uNFivs@NWnyn(AIhu_Ur1-=S~ zz*qsy>n%H5(>!g8B>7fBI`m-Mro-e^DLtsFz>6n<+o2ZA0Rexpku>#H0j z;!Ke?18_6eb2Sj1k+x15d29W|`tqtXuZ`-w1=SGk=R*wjtkhQ5b1LKkKa7~B?tzET ze_sbtK(3Uux%^^viB1%!3clooPurxsM}!u?e>=esB`sLYE{%+F{(U$~7;OY>Y+RDb zjUyK!7F9wT005OU0NP|9!f!o|guZBHV^CeSZ0ND>{qUJ7!Ik^`R35B#D7TU3?Jv#M zmlFK*MRkMJzKvUi@)Ir59jqrfhRZ8yB!{*t+#_f?N?| z)ol5jrJ$oDS?4pemy0Vf4$oa*pANz?+vnl!l&HD!>A0_FFDhrG->&{k8Y7+*cjCX+ zscu5ez3?VweP*{d3Ucuw$WiY;xBA?2Dy?4;@G4C>`P#DwnFtT*he` zp!{%@J!$(;YlT`%{sK@csAl_II4eRPiur|zTg3@i+rGo@rNE{K%o40^Wadgy8l{TJ zmiABGtE-&umLV%8?N!i99YoJ0M@BgD*5IzR5~c#^pmq#a7}r(A@qR%6R2J3hFV64j%Kv~ujZO#G&${EM%9X85XU(6=G^ zxnXnvW)S>QtG(W9U_$I^;wLmZB0aA1r}Iyp*CMgbp!?LL>?rNvq$Y*!y28@bQs2O{@)wLphxfjku!eLV zc#z-E-zBja+jIh0x~8XC%b#pGO5?uHAu1`!1u7h{?Ci zPOtZiX>0eCiRe+Ceq`a+47N`p^l1L8VTp-$p%{-wCJGer)i+!;#?~v)(>Upk0Npe0 z+a+ABxp$w4XZ5^jhRfBp6|BT|Q}Rnf=a8cIJVZFLjDAZ;$jWz7%0J?@_e zJdLmV2vz7}gq6ZdtTo*f5=gDAFTOv0`sy0ZOfg2gRF-3~UZM1t$bR|a)azZT#a=ze zrN3^@)TZAyROH_>{sGydsC*H{R%K@=W{_cLK8}!*iinAqNKLubD9G$QR$4`0bp{LM zvJrjJB&w-j7Ngq?rY!F6$4!c83V-?i#_`s3QG=%9+YPRgZcckSX>Zt##VQ4F*B7Ej zQFWnQH43eY6aa2>7bFQt&4>eR{t}AB0fEU=w0TOx3>J5}feBSwzAVgqm#6vg`S>l| z4a31ZS~)w8pC>R?_#QYjyFl9z91KT3EUe)Uw((-Ma1smg3iAvuRvI@j^ez?@avLO# zbp=625uV;ZQQ+VFul$eV^WZJ(v-SHD)%FIDC&s+2Q<(+R^?>PG3i*`?#71wVL2s_o zZ(K!}&lu(XwccVOaBnl*M#a<6nk{*^}@5_lG^cbQfPmyY>p1&>Yo<+ z*ro{;QF*+mfpEri_eW#%v~ZOWrVeCtFmtE-*BK@71T&n?eB55@RZVW&>{J-(pajsc z6M|0?Wcn<#@=#%%KGF{Qw_3;qch-PDMww})dqY7MnA}%nE933Of^fd}Ih_d^Y$Njy ze(@e<8Xn)FPdpc)zU2&n2DiFRLDFfXBxk5k?8fH%HEDmJ$bM~d%((LT;GQ~Og=WZu zLP>%ZcD+O9@h3)4KgPZ-t!CBhyLkLxf!<$Fg#WL~`@cBwe@3Ht>5N(sk!+-QzLiL` PR$emHHPxxndK~p1+Y3%d literal 0 HcmV?d00001 diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dd07cb7ca7c072807d5607825e08ccb0ca51d7ca GIT binary patch literal 21248 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 PR$emHHPxxndK~p1+Y3%d literal 0 HcmV?d00001 diff --git a/configuration.json b/configuration.json index ba24166..e4a2d99 100644 --- a/configuration.json +++ b/configuration.json @@ -10,17 +10,17 @@ "type": "input" }, { - "defaultValue": "", + "defaultValue": "C:\\Program Files\\Microsoft Dynamics 365 Business Central\\Service\\navadmintool.ps1", "key": "PSModulePath", "templateOptions": { - "description": "", + "description": "Path to powershell NavAdminTools module", "label": "Powershell module path", "required": true }, "type": "input" }, { - "defaultValue": "", + "defaultValue": "DEERP", "key": "NavServerInstanceName", "templateOptions": { "description": "NavServer Instance Name", @@ -30,10 +30,10 @@ "type": "input" }, { - "defaultValue": "", + "defaultValue": "Default", "key": "Tenant", "templateOptions": { - "description": "Tenant", + "description": "Business Central Tenant, usually default", "label": "Tenant", "required": true }, diff --git a/create.ps1 b/create.ps1 index c04b2f8..226b1d3 100644 --- a/create.ps1 +++ b/create.ps1 @@ -54,12 +54,13 @@ try { $EmailAddress = $actionContext.Data.EmailAddress $SamAccountName = $actionContext.Data.Username $UserPrincipalName = $actionContext.Data.userPrincipalName - $Displayname = $actionContext.Data.Displayname + $Fullname = $actionContext.Data.FullName $PSPath = $actionContext.Configuration.PSModulePath $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName $tenant = $actionContext.Configuration.Tenant + # Import module $Import = Invoke-Command -Session $s -ScriptBlock { param($ImportPath) @@ -118,13 +119,13 @@ try { -ServerInstance $ServerInstance ` -Tenant $Tenant ` -WindowsAccount $SamAccountName ` - -FullName $Displayname ` + -FullName $Fullname ` -AuthenticationEmail $UserPrincipalName ` -ContactEmail $EmailAddress ` - -State Disabled ` + -State Enabled ` -Verbose } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Displayname, $UserPrincipalName, $EmailAddress - + #Set permissions <# $setPermissions = Invoke-Command -Session $s -ScriptBlock { @@ -151,8 +152,8 @@ try { Where-Object { $_.UserName -eq $UserName } } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName - $outputContext.Data.SecurityID = $result.UserSecurityID - $outputContext.Data.DisplayName = $result.Fullname + $outputContext.Data.UserSecurityID = $result.UserSecurityID + $outputContext.Data.FullName = $result.Fullname $outputContext.Data.UserName = $result.Username $auditLogMessage = "Create account was successful. AccountReference is: [$($outputContext.Data.SecurityID)]" @@ -167,8 +168,8 @@ try { 'CorrelateAccount' { Write-Information 'Correlating Nav User' - $outputContext.Data.SecurityID = $correlatedAccount.UserSecurityID - $outputContext.Data.DisplayName = $correlatedAccount.Fullname + $outputContext.Data.UserSecurityID = $correlatedAccount.UserSecurityID + $outputContext.Data.Fullname = $correlatedAccount.Fullname $outputContext.Data.UserName = $correlatedAccount.Username $outputContext.AccountCorrelated = $true @@ -178,8 +179,8 @@ try { } $accountRef = [PSCustomObject]@{ - SecurityID = $outputContext.Data.SecurityID - DisplayName = $outputContext.Data.DisplayName + UserSecurityID = $outputContext.Data.UserSecurityID + Fullname = $outputContext.Data.Fullname UserName = $outputContext.Data.UserName } @@ -213,4 +214,4 @@ catch { Message = $auditMessage IsError = $true }) -} +} \ No newline at end of file diff --git a/delete.ps1 b/delete.ps1 index f2c716c..53d960f 100644 --- a/delete.ps1 +++ b/delete.ps1 @@ -56,7 +56,7 @@ try { $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver # Define variables - $SamAccountName = $actionContext.Data.Username + $SamAccountName = $actionContext.References.Account.userName $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName $PSPath = $actionContext.Configuration.PSModulePath $tenant = $actionContext.Configuration.Tenant diff --git a/enable.ps1 b/enable.ps1 index 87dbb9e..0234307 100644 --- a/enable.ps1 +++ b/enable.ps1 @@ -56,7 +56,7 @@ try { $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver # Define variables - $SamAccountName = $actionContext.Data.Username + $SamAccountName = $actionContext.References.Account.userName $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName $PSPath = $actionContext.Configuration.PSModulePath $tenant = $actionContext.Configuration.Tenant diff --git a/fieldmapping.json b/fieldmapping.json index 2c48fdf..09daf87 100644 --- a/fieldmapping.json +++ b/fieldmapping.json @@ -10,18 +10,17 @@ "MapForActions": [ "Create", "Update", - "Delete", "Enable" ], "MappingMode": "Complex", - "Value": "\"function getValue(){\\r\\n let costcenter = Person.PrimaryContract.CostCenter.Code\\r\\n let company = ''\\r\\n\\r\\n return company\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "Value": "\"function getValue(){\\r\\n //let costcenter = Person.PrimaryContract.CostCenter.Code\\r\\n let company = 'Bedrijfsnaam'\\r\\n\\r\\n return company\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", "UsedInNotifications": false, "StoreInAccountData": false } ] }, { - "Name": "Username", + "Name": "EmailAddress", "Description": "", "Type": "Text", "MappingActions": [ @@ -29,11 +28,10 @@ "MapForActions": [ "Create", "Update", - "Delete", "Enable" ], "MappingMode": "Complex", - "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n \\r\\n let domain = 'DOMAIN\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.mail\\r\\n\\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", "UsedInNotifications": false, "StoreInAccountData": true } @@ -48,7 +46,6 @@ "MapForActions": [ "Create", "Update", - "Delete", "Enable" ], "MappingMode": "Complex", @@ -59,7 +56,7 @@ ] }, { - "Name": "EmailAddress", + "Name": "Username", "Description": "", "Type": "Text", "MappingActions": [ @@ -71,14 +68,14 @@ "Enable" ], "MappingMode": "Complex", - "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.mail\\r\\n\\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", + "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n\\r\\n let domain = 'DOMAIN\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", "UsedInNotifications": false, "StoreInAccountData": true } ] }, { - "Name": "DisplayName", + "Name": "UserSecurityID", "Description": "", "Type": "Text", "MappingActions": [ @@ -86,18 +83,17 @@ "MapForActions": [ "Create", "Update", - "Delete", "Enable" ], - "MappingMode": "Complex", - "Value": "\"function generateDisplayName() {\\r\\n\\r\\n let nickName = Person.Name.NickName;\\r\\n let middleName = Person.Name.FamilyNamePrefix;\\r\\n let lastName = Person.Name.FamilyName;\\r\\n let middleNamePartner = Person.Name.FamilyNamePartnerPrefix;\\r\\n let lastNamePartner = Person.Name.FamilyNamePartner;\\r\\n let convention = Person.Name.Convention;\\r\\n\\r\\n let displayName = '';\\r\\n switch (convention) {\\r\\n case \\\"BP\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n\\r\\n displayName = displayName + ' - ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n break;\\r\\n case \\\"PB\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n\\r\\n displayName = displayName + ' - ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n break;\\r\\n case \\\"P\\\":\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { displayName = displayName + middleNamePartner + ' ' }\\r\\n displayName = displayName + lastNamePartner;\\r\\n break;\\r\\n case \\\"B\\\":\\r\\n default:\\r\\n displayName = displayName + nickName + ' ';\\r\\n if (typeof middleName !== 'undefined' && middleName) { displayName = displayName + middleName + ' ' }\\r\\n displayName = displayName + lastName;\\r\\n break;\\r\\n }\\r\\n // Trim spaces at start and end\\r\\n displayName = displayName.trim();\\r\\n\\r\\n // Shorten string to maxAttributeLength \\r\\n const maxAttributeLength = 256;\\r\\n displayName = displayName.substring(0, maxAttributeLength);\\r\\n \\r\\n return displayName;\\r\\n}\\r\\n\\r\\ngenerateDisplayName();\"", + "MappingMode": "Fixed", + "Value": "\"\"", "UsedInNotifications": false, "StoreInAccountData": true } ] }, { - "Name": "SecurityID", + "Name": "FullName", "Description": "", "Type": "Text", "MappingActions": [ @@ -105,11 +101,10 @@ "MapForActions": [ "Create", "Update", - "Delete", "Enable" ], - "MappingMode": "Fixed", - "Value": "\"\"", + "MappingMode": "Complex", + "Value": "\"// Please enter the mapping logic to generate the commonName based on name convention.\\r\\nfunction generateDisplayName() {\\r\\n\\tlet firstName = Person.Name.NickName;\\r\\n\\tlet middleName = Person.Name.FamilyNamePrefix;\\r\\n\\tlet lastName = Person.Name.FamilyName;\\r\\n\\tlet middleNamePartner = Person.Name.FamilyNamePartnerPrefix;\\r\\n\\tlet lastNamePartner = Person.Name.FamilyNamePartner;\\r\\n let nameFormatted = '';\\r\\n\\r\\n\\tswitch(Person.Name.Convention) {\\r\\n\\t\\tcase 'B':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\r\\n\\t\\t\\tbreak;\\r\\n\\t\\tcase 'BP':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName + ' -'\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner\\r\\n\\t\\t\\tbreak;\\r\\n\\t\\tcase 'P':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner\\r\\n\\t\\t break;\\r\\n\\t\\tcase 'PB':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner + ' -'\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\t\\t break;\\r\\n\\t\\tdefault:\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\t\\t\\tbreak;\\r\\n\\t}\\r\\n\\r\\n\\tconst displayName = nameFormatted;\\r\\n\\r\\n\\treturn displayName;\\r\\n}\\r\\n\\r\\ngenerateDisplayName();\\r\\n\\r\\n\"", "UsedInNotifications": false, "StoreInAccountData": true } diff --git a/update.ps1 b/update.ps1 index 7a075ff..da268d7 100644 --- a/update.ps1 +++ b/update.ps1 @@ -1,5 +1,6 @@ ################################################# # HelloID-Conn-Prov-Target-DynamicsEmpire-Update +# Update is optional as this can be done through authorizationrequests # PowerShell V2 ################################################# @@ -56,7 +57,7 @@ try { $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver # Define variables - $SamAccountName = $actionContext.Data.Username + $SamAccountName = $actionContext.References.Account.userName $Displayname = $actionContext.Data.DisplayName $UserPrincipalName = $actionContext.Data.userPrincipalName $EmailAddress = $actionContext.Data.EmailAddress @@ -107,7 +108,7 @@ try { } } - $outputContext.Data.SecurityID = $($actionContext.References.Account.SecurityID) + $outputContext.Data.UserSecurityID = $($actionContext.References.Account.UserSecurityID) $outputContext.Success = $true $outputContext.AuditLogs.Add([PSCustomObject]@{ From 95b31a49c04a6b845f734dccebe3a4aef5f62e8d Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:06:28 +0100 Subject: [PATCH 05/10] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a95c29d..a5223ab 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ > This connector must be used in conjunction with the [HelloID-Conn-Prov-Target-Authorizationbox](https://github.com/Tools4everBV/HelloID-Conn-Prov-Target-Authorizationbox) connector.

- +

## Table of contents From 7e0ffc34e2c624c931327b5f5cf64e41fbfbdf41 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:25:47 +0100 Subject: [PATCH 06/10] Update create.ps1 Hotfix for displayname --- create.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/create.ps1 b/create.ps1 index 226b1d3..4e7c617 100644 --- a/create.ps1 +++ b/create.ps1 @@ -124,7 +124,7 @@ try { -ContactEmail $EmailAddress ` -State Enabled ` -Verbose - } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Displayname, $UserPrincipalName, $EmailAddress + } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Fullname, $UserPrincipalName, $EmailAddress #Set permissions <# @@ -214,4 +214,4 @@ catch { Message = $auditMessage IsError = $true }) -} \ No newline at end of file +} From 6d1ecc92ab08e1fbdc48fb936cc3654024510e75 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:28:37 +0100 Subject: [PATCH 07/10] Update create.ps1 Hotfix for fullname 2 --- create.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create.ps1 b/create.ps1 index 4e7c617..5746999 100644 --- a/create.ps1 +++ b/create.ps1 @@ -119,7 +119,7 @@ try { -ServerInstance $ServerInstance ` -Tenant $Tenant ` -WindowsAccount $SamAccountName ` - -FullName $Fullname ` + -FullName $Displayname ` -AuthenticationEmail $UserPrincipalName ` -ContactEmail $EmailAddress ` -State Enabled ` From 6df767da64ee52f8f0fa8ab5ac3343e7941a8d50 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:29:30 +0100 Subject: [PATCH 08/10] Update create.ps1 typo --- create.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/create.ps1 b/create.ps1 index 5746999..a0d3229 100644 --- a/create.ps1 +++ b/create.ps1 @@ -156,7 +156,7 @@ try { $outputContext.Data.FullName = $result.Fullname $outputContext.Data.UserName = $result.Username - $auditLogMessage = "Create account was successful. AccountReference is: [$($outputContext.Data.SecurityID)]" + $auditLogMessage = "Create account was successful. AccountReference is: [$($outputContext.Data.UserSecurityID)]" } else { @@ -173,7 +173,7 @@ try { $outputContext.Data.UserName = $correlatedAccount.Username $outputContext.AccountCorrelated = $true - $auditLogMessage = "Correlated account: [$($actionContext.Data.Username)]. AccountReference is: [$($outputContext.Data.SecurityID)]" + $auditLogMessage = "Correlated account: [$($actionContext.Data.Username)]. AccountReference is: [$($outputContext.Data.UserSecurityID)]" break } } From f5b967287919e01adef851b64b6af0597f858ba8 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:45:57 +0200 Subject: [PATCH 09/10] Rework v2 Only create actions remain as enable, update, delete are managed through the Authorizationbox connector. --- create.ps1 | 2 +- delete.ps1 | 118 -------------------------------------- enable.ps1 | 118 -------------------------------------- fieldmapping.json | 39 ++----------- readme.md | 14 ++--- update.ps1 | 140 ---------------------------------------------- 6 files changed, 13 insertions(+), 418 deletions(-) delete mode 100644 delete.ps1 delete mode 100644 enable.ps1 delete mode 100644 update.ps1 diff --git a/create.ps1 b/create.ps1 index a0d3229..bf31331 100644 --- a/create.ps1 +++ b/create.ps1 @@ -126,7 +126,7 @@ try { -Verbose } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Fullname, $UserPrincipalName, $EmailAddress - #Set permissions + #Set permissions <# $setPermissions = Invoke-Command -Session $s -ScriptBlock { param( diff --git a/delete.ps1 b/delete.ps1 deleted file mode 100644 index 53d960f..0000000 --- a/delete.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -################################################## -# HelloID-Conn-Prov-Target-EmpireDynamics-Delete -# PowerShell V2 -################################################## - -# Enable TLS1.2 -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 - -#region functions -function Resolve-EmpireError { - [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) - # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. - # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment - } - catch { - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails - } - Write-Output $httpErrorObj - } -} -#endregion - -try { - # Verify if [aRef] has a value - if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { - throw 'The account reference could not be found' - } - - # Create session - $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver - - # Define variables - $SamAccountName = $actionContext.References.Account.userName - $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName - $PSPath = $actionContext.Configuration.PSModulePath - $tenant = $actionContext.Configuration.Tenant - - # Import module - $Import = Invoke-Command -Session $s -ScriptBlock { - param($ImportPath) - Import-Module $ImportPath -ErrorAction Stop - } -ArgumentList $PSPath - - $action = 'DeleteAccount' - - switch ($action) { - 'DeleteAccount' { - if (-not($actionContext.DryRun -eq $true)) { - # Disable user - Invoke-Command -Session $s -ScriptBlock { - param($ServerInstance, $Tenant, $UserName) - Set-NAVServerUser ` - -ServerInstance $ServerInstance ` - -Tenant $Tenant ` - -WindowsAccount $UserName ` - -State Disabled ` - -ErrorAction Stop ` - -Verbose - } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName - - } - else { - Write-Information "[DryRun] Would delete (disable) NAV user [$SamAccountName]" - } - } - } - - $outputContext.Success = $true - - #Remove session - $exit = if ($s) { Remove-PSSession $s } - -} -catch { - $outputContext.success = $false - $ex = $PSItem - $exit = if ($s) { Remove-PSSession $s } - if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or - $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { - $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not delete EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" - Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" - } - else { - $auditMessage = "Could not delete EmpireDynamics account. Error: $($ex.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 - }) -} \ No newline at end of file diff --git a/enable.ps1 b/enable.ps1 deleted file mode 100644 index 0234307..0000000 --- a/enable.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -################################################## -# HelloID-Conn-Prov-Target-EmpireDynamics-Enable -# PowerShell V2 -################################################## - -# Enable TLS1.2 -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 - -#region functions -function Resolve-EmpireError { - [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) - # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. - # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment - } - catch { - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails - } - Write-Output $httpErrorObj - } -} -#endregion - -try { - # Verify if [aRef] has a value - if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { - throw 'The account reference could not be found' - } - - # Create session - $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver - - # Define variables - $SamAccountName = $actionContext.References.Account.userName - $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName - $PSPath = $actionContext.Configuration.PSModulePath - $tenant = $actionContext.Configuration.Tenant - - # Import module - $Import = Invoke-Command -Session $s -ScriptBlock { - param($ImportPath) - Import-Module $ImportPath -ErrorAction Stop - } -ArgumentList $PSPath - - $action = 'EnableAccount' - - switch ($action) { - 'EnableAccount' { - if (-not($actionContext.DryRun -eq $true)) { - # Enable user - Invoke-Command -Session $s -ScriptBlock { - param($ServerInstance, $Tenant, $UserName) - Set-NAVServerUser ` - -ServerInstance $ServerInstance ` - -Tenant $Tenant ` - -WindowsAccount $UserName ` - -State Enabled ` - -ErrorAction Stop ` - -Verbose - } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName - - } - else { - Write-Information "[DryRun] Would enable NAV user [$SamAccountName]" - } - } - } - - $outputContext.Success = $true - - #Remove session - $exit = if ($s) { Remove-PSSession $s } - -} -catch { - $outputContext.success = $false - $ex = $PSItem - $exit = if ($s) { Remove-PSSession $s } - if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or - $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { - $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not enable EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" - Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" - } - else { - $auditMessage = "Could not enable EmpireDynamics account. Error: $($ex.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 - }) -} \ No newline at end of file diff --git a/fieldmapping.json b/fieldmapping.json index 09daf87..c42a3a1 100644 --- a/fieldmapping.json +++ b/fieldmapping.json @@ -1,24 +1,6 @@ { "Version": "v1", "MappingFields": [ - { - "Name": "DefaultCompany", - "Description": "", - "Type": "Text", - "MappingActions": [ - { - "MapForActions": [ - "Create", - "Update", - "Enable" - ], - "MappingMode": "Complex", - "Value": "\"function getValue(){\\r\\n //let costcenter = Person.PrimaryContract.CostCenter.Code\\r\\n let company = 'Bedrijfsnaam'\\r\\n\\r\\n return company\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", - "UsedInNotifications": false, - "StoreInAccountData": false - } - ] - }, { "Name": "EmailAddress", "Description": "", @@ -26,9 +8,7 @@ "MappingActions": [ { "MapForActions": [ - "Create", - "Update", - "Enable" + "Create" ], "MappingMode": "Complex", "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.mail\\r\\n\\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", @@ -44,9 +24,7 @@ "MappingActions": [ { "MapForActions": [ - "Create", - "Update", - "Enable" + "Create" ], "MappingMode": "Complex", "Value": "\"function getValue(){\\r\\n let email = Person.Accounts.MicrosoftActiveDirectory.userPrincipalName\\r\\n \\r\\n return email\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", @@ -62,10 +40,7 @@ "MappingActions": [ { "MapForActions": [ - "Create", - "Update", - "Delete", - "Enable" + "Create" ], "MappingMode": "Complex", "Value": "\"function getValue(){\\r\\n let username = Person.Accounts.MicrosoftActiveDirectory.sAMAccountName\\r\\n\\r\\n let domain = 'DOMAIN\\\\\\\\'\\r\\n\\r\\n return domain + username\\r\\n\\r\\n}\\r\\n\\r\\ngetValue()\"", @@ -81,9 +56,7 @@ "MappingActions": [ { "MapForActions": [ - "Create", - "Update", - "Enable" + "Create" ], "MappingMode": "Fixed", "Value": "\"\"", @@ -99,9 +72,7 @@ "MappingActions": [ { "MapForActions": [ - "Create", - "Update", - "Enable" + "Create" ], "MappingMode": "Complex", "Value": "\"// Please enter the mapping logic to generate the commonName based on name convention.\\r\\nfunction generateDisplayName() {\\r\\n\\tlet firstName = Person.Name.NickName;\\r\\n\\tlet middleName = Person.Name.FamilyNamePrefix;\\r\\n\\tlet lastName = Person.Name.FamilyName;\\r\\n\\tlet middleNamePartner = Person.Name.FamilyNamePartnerPrefix;\\r\\n\\tlet lastNamePartner = Person.Name.FamilyNamePartner;\\r\\n let nameFormatted = '';\\r\\n\\r\\n\\tswitch(Person.Name.Convention) {\\r\\n\\t\\tcase 'B':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\r\\n\\t\\t\\tbreak;\\r\\n\\t\\tcase 'BP':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName + ' -'\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner\\r\\n\\t\\t\\tbreak;\\r\\n\\t\\tcase 'P':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner\\r\\n\\t\\t break;\\r\\n\\t\\tcase 'PB':\\r\\n nameFormatted = firstName\\r\\n if (typeof middleNamePartner !== 'undefined' && middleNamePartner) { nameFormatted = nameFormatted + ' ' + middleNamePartner }\\r\\n nameFormatted = nameFormatted + ' ' + lastNamePartner + ' -'\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\t\\t break;\\r\\n\\t\\tdefault:\\r\\n nameFormatted = firstName\\r\\n if (typeof middleName !== 'undefined' && middleName) { nameFormatted = nameFormatted + ' ' + middleName }\\r\\n nameFormatted = nameFormatted + ' ' + lastName\\r\\n\\t\\t\\tbreak;\\r\\n\\t}\\r\\n\\r\\n\\tconst displayName = nameFormatted;\\r\\n\\r\\n\\treturn displayName;\\r\\n}\\r\\n\\r\\ngenerateDisplayName();\\r\\n\\r\\n\"", diff --git a/readme.md b/readme.md index a5223ab..4e97999 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ - [HelloID-Conn-Prov-Target-DynamicsEmpire](#helloid-conn-prov-target-dynamicsempire) - [Table of contents](#table-of-contents) - [Introduction](#introduction) - - [Supported features](#supported--features) + - [Supported features](#supported-features) - [Getting started](#getting-started) - [HelloID Icon URL](#helloid-icon-url) - [Requirements](#requirements) @@ -40,13 +40,13 @@ _HelloID-Conn-Prov-Target-DynamicsEmpire_ is a target connector. DynamicsEmpire provides a set of PowerShell functions allowing you to programmatically interact with its data. -## Supported features +## Supported features The following features are available: | Feature | Supported | Actions | Remarks | | ----------------------------------------- | --------- | --------------------------------------- | ----------------- | -| **Account Lifecycle** | ✅ | Create, Enable, Update, Delete | Deletion is currently only disabling the user | +| **Account Lifecycle** | ✅ | Create, Correlate | Correlation is mandatory. | | **Permissions** | ❌ | - | | | **Resources** | ❌ | - | | | **Entitlement Import: Accounts** | ❌ | - | | @@ -61,12 +61,12 @@ Governance Reconciliation Resolutions are not yet available. ### HelloID Icon URL URL of the icon used for the HelloID Provisioning target system. ``` -https://raw.githubusercontent.com/Tools4everBV/HelloID-Conn-Prov-Target-ActiveDirectory/refs/heads/main/Icon.png +https://raw.githubusercontent.com/Tools4everBV/HelloID-Conn-Prov-Target-DynamicsEmpire/refs/heads/main/Icon.png ``` ### Requirements -- Service account for the HelloID Agent with sufficient rights within Empire to create, update, disable, and delete accounts. +- Service account for the HelloID Agent with sufficient rights within Empire to create accounts. - Service account for the HelloID Agent must be able to connect to the instance where Empire is installed. - Linked usage with AuthorizationBox connector @@ -77,7 +77,7 @@ The following settings are required to connect to DynamicsEmpire. | Setting | Description | Mandatory | | --------------------- | -------------------------------------------------------- | --------- | | PSModulePath | Full path to the `NavAdminTools.ps1` file | Yes | -| ApplicationServer | Name of the Dynamics NAV application server | Yes | +| Applicationserver | Name of the Dynamics NAV application server | Yes | | NavServerInstanceName | Name of the Dynamics NAV server instance | Yes | | Tenant | ID of the Dynamics NAV tenant | Yes | @@ -105,7 +105,7 @@ It is advised not to change the reference after creation or correlation due to i ## Remarks -- Service user that runs the agent must have sufficient rights within Empire to create, update, and delete accounts. +- Service user that runs the agent must have sufficient rights within Empire to create and correlate accounts. - This connector is usually linked to the AuthorizationBox connector, as Security ID is required there. ### Dotsourcing of the `NavAdminTools.ps1` file diff --git a/update.ps1 b/update.ps1 deleted file mode 100644 index da268d7..0000000 --- a/update.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -################################################# -# HelloID-Conn-Prov-Target-DynamicsEmpire-Update -# Update is optional as this can be done through authorizationrequests -# PowerShell V2 -################################################# - -# Enable TLS1.2 -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 - -#region functions -function Resolve-EmpireError { - [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) - # Make sure to inspect the error result object and add only the error message as a FriendlyMessage. - # $httpErrorObj.FriendlyMessage = $errorDetailsObject.message - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails # Temporarily assignment - } - catch { - $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails - } - Write-Output $httpErrorObj - } -} -#endregion - -try { - # Verify if [aRef] has a value - if ([string]::IsNullOrEmpty($($actionContext.References.Account))) { - throw 'The account reference could not be found' - } - - # Create session - $s = New-PSSession -ComputerName $actionContext.Configuration.Applicationserver - - # Define variables - $SamAccountName = $actionContext.References.Account.userName - $Displayname = $actionContext.Data.DisplayName - $UserPrincipalName = $actionContext.Data.userPrincipalName - $EmailAddress = $actionContext.Data.EmailAddress - - $PSPath = $actionContext.Configuration.PSModulePath - $NavServerInstanceName = $actionContext.Configuration.NavServerInstanceName - $tenant = $actionContext.Configuration.Tenant - - # Make sure module is imported - $Import = Invoke-Command -Session $s -ScriptBlock { - param($ImportPath) - Import-Module $ImportPath -ErrorAction SilentlyContinue - } -ArgumentList $PSPath - - # Compare can be made but Nav handles this. We keep this $action variable for when we want to expend on the actions. - $action = 'UpdateAccount' - - switch ($action) { - 'UpdateAccount' { - Write-Information "Updating Nav account with accountReference: [$($actionContext.References.Account)]" - - if (-not($actionContext.DryRun -eq $true)) { - $UpdateAction = Invoke-Command -Session $s -ScriptBlock { - param( - $ServerInstance, - $Tenant, - $SamAccountName, - $Displayname, - $UserPrincipalName, - $EmailAddress - ) - Set-NAVServerUser ` - -ServerInstance $ServerInstance ` - -Tenant $Tenant ` - -WindowsAccount $SamAccountName ` - -FullName $Displayname ` - -AuthenticationEmail $UserPrincipalName ` - -ContactEmail $EmailAddress ` - -ErrorAction Inquire ` - -Verbose ` - -Force - } -ArgumentList $NavServerInstanceName, $tenant, $SamAccountName, $Displayname, $UserPrincipalName, $EmailAddress - } - else { - Write-Information "Dryrun prevented this action" - } - - } - } - - $outputContext.Data.UserSecurityID = $($actionContext.References.Account.UserSecurityID) - - $outputContext.Success = $true - $outputContext.AuditLogs.Add([PSCustomObject]@{ - Message = "Update account was successfull" - IsError = $false - }) - - #Remove session - $exit = Remove-PSSession $s -} -catch { - $outputContext.Success = $false - $ex = $PSItem - $exit = Remove-PSSession $s - if ($($ex.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') -or - $($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) { - $errorObj = Resolve-EmpireError -ErrorObject $ex - $auditMessage = "Could not update EmpireDynamics account. Error: $($errorObj.FriendlyMessage)" - Write-Warning "Error at Line '$($errorObj.ScriptLineNumber)': $($errorObj.Line). Error: $($errorObj.ErrorDetails)" - } - else { - $auditMessage = "Could not update EmpireDynamics account. Error: $($ex.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 - }) -} From e9a8bd0567e49cf310186d1d2bdfd2cebd13c235 Mon Sep 17 00:00:00 2001 From: rscholtelubberink <80954753+rscholtelubberink@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:54:20 +0200 Subject: [PATCH 10/10] Added workflows/changelog Added workflows/changelog --- .github/workflows/createRelease.yaml | 9 ++++++ .github/workflows/verifyChangelog.yaml | 8 +++++ CHANGELOG.md | 43 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 .github/workflows/createRelease.yaml create mode 100644 .github/workflows/verifyChangelog.yaml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/createRelease.yaml b/.github/workflows/createRelease.yaml new file mode 100644 index 0000000..472a1f6 --- /dev/null +++ b/.github/workflows/createRelease.yaml @@ -0,0 +1,9 @@ +name: Workflow caller - create release +on: + pull_request: + types: [closed] + +jobs: + verify-changelog: + if: github.event.pull_request.merged == true + uses: Tools4everBV/.github/.github/workflows/createRelease.yaml@main diff --git a/.github/workflows/verifyChangelog.yaml b/.github/workflows/verifyChangelog.yaml new file mode 100644 index 0000000..4913991 --- /dev/null +++ b/.github/workflows/verifyChangelog.yaml @@ -0,0 +1,8 @@ +name: Workflow caller - verify changelog updated +on: + pull_request: + types: [opened, synchronize] + +jobs: + verify-changelog: + uses: Tools4everBV/.github/.github/workflows/verifyChangelog.yaml@main diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b63e173 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Change Log + +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). + +## [2.0.0] - 2026-06-04 + +Reworked release of _HelloID-Conn-Prov-Target-DynamicsEmpire_ . + +### Added + +- Updated documentation and connector metadata for the V2 target connector structure. + +### Changed + +- Reworked provisioning implementation to align with HelloID PowerShell V2 conventions. +- Updated field mapping to match current repository behavior for create flow. +- Updated README content to reflect current connector scope and configuration. + +### Deprecated + +### Removed + +- Legacy template placeholders and outdated references from documentation. + +## [1.0.0] - 2026-06-04 + +Initial (legacy) release line of _HelloID-Conn-Prov-Target-DynamicsEmpire_ before the PowerShell V2 rework. + +### Added + +- Initial connector implementation and base project files. + +### Changed + +- No further changes recorded for this legacy baseline. + +### Deprecated + +- Legacy implementation line, superseded by 2.0.0. + +### Removed + +- Not applicable. \ No newline at end of file