Skip to content

Commit

Permalink
Fixed issue with keepInSyncWith and clarified some comments for $fiel…
Browse files Browse the repository at this point in the history
…dsToCheck object (#20)
  • Loading branch information
rschouten97 authored Nov 20, 2024
1 parent 2303b17 commit 0574a8c
Showing 1 changed file with 109 additions and 162 deletions.
271 changes: 109 additions & 162 deletions uniquenessCheck.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Check if fields are unique
# PowerShell V2
#################################################

# Enable TLS1.2
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

Expand All @@ -15,38 +16,12 @@ $InformationPreference = "Continue"
$WarningPreference = "Continue"

#region functions
function Convert-StringToBoolean($obj) {
if ($obj -is [PSCustomObject]) {
foreach ($property in $obj.PSObject.Properties) {
$value = $property.Value
if ($value -is [string]) {
$lowercaseValue = $value.ToLower()
if ($lowercaseValue -eq "true") {
$obj.$($property.Name) = $true
}
elseif ($lowercaseValue -eq "false") {
$obj.$($property.Name) = $false
}
}
elseif ($value -is [PSCustomObject] -or $value -is [System.Collections.IDictionary]) {
$obj.$($property.Name) = Convert-StringToBoolean $value
}
elseif ($value -is [System.Collections.IList]) {
for ($i = 0; $i -lt $value.Count; $i++) {
$value[$i] = Convert-StringToBoolean $value[$i]
}
$obj.$($property.Name) = $value
}
}
}
return $obj
}

function Resolve-MicrosoftGraphAPIError {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[object] $ErrorObject
[object]
$ErrorObject
)
process {
$httpErrorObj = [PSCustomObject]@{
Expand All @@ -55,36 +30,33 @@ function Resolve-MicrosoftGraphAPIError {
ErrorDetails = $ErrorObject.Exception.Message
FriendlyMessage = $ErrorObject.Exception.Message
}

if (-not [string]::IsNullOrEmpty($ErrorObject.ErrorDetails.Message)) {
$httpErrorObj.ErrorDetails = $ErrorObject.ErrorDetails.Message
}
elseif ($ErrorObject.Exception -is [System.Net.WebException] -and $ErrorObject.Exception.Response) {
$streamReaderResponse = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd()
if (-not [string]::IsNullOrEmpty($streamReaderResponse)) {
$httpErrorObj.ErrorDetails = $streamReaderResponse
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 {
$errorObjectConverted = $httpErrorObj.ErrorDetails | ConvertFrom-Json -ErrorAction Stop
$errorObjectConverted = $ErrorObject | ConvertFrom-Json -ErrorAction Stop

if ($errorObjectConverted.error_description) {
if ($null -ne $errorObjectConverted.error_description) {
$httpErrorObj.FriendlyMessage = $errorObjectConverted.error_description
}
elseif ($errorObjectConverted.error) {
$httpErrorObj.FriendlyMessage = $errorObjectConverted.error.message
if ($errorObjectConverted.error.code) {
$httpErrorObj.FriendlyMessage += " Error code: $($errorObjectConverted.error.code)."
}
if ($errorObjectConverted.error.details) {
if ($errorObjectConverted.error.details.message) {
$httpErrorObj.FriendlyMessage += " Details message: $($errorObjectConverted.error.details.message)"
}
if ($errorObjectConverted.error.details.code) {
$httpErrorObj.FriendlyMessage += " Details code: $($errorObjectConverted.error.details.code)."
elseif ($null -ne $errorObjectConverted.error) {
if ($null -ne $errorObjectConverted.error.message) {
$httpErrorObj.FriendlyMessage = $errorObjectConverted.error.message
if ($null -ne $errorObjectConverted.error.code) {
$httpErrorObj.FriendlyMessage = $httpErrorObj.FriendlyMessage + " Error code: $($errorObjectConverted.error.code)"
}
}
else {
$httpErrorObj.FriendlyMessage = $errorObjectConverted.error
}
}
else {
$httpErrorObj.FriendlyMessage = $ErrorObject
Expand All @@ -93,174 +65,147 @@ function Resolve-MicrosoftGraphAPIError {
catch {
$httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails
}

Write-Output $httpErrorObj
}
}

function New-AuthorizationHeaders {
[CmdletBinding()]
[OutputType([System.Collections.Generic.Dictionary[[String], [String]]])]
param(
[parameter(Mandatory)]
[string]
$TenantId,

[parameter(Mandatory)]
[string]
$ClientId,

[parameter(Mandatory)]
[string]
$ClientSecret
)
try {
Write-Verbose "Creating Access Token"
$baseUri = "https://login.microsoftonline.com/"
$authUri = $baseUri + "$TenantId/oauth2/token"

$body = @{
grant_type = "client_credentials"
client_id = "$ClientId"
client_secret = "$ClientSecret"
resource = "https://graph.microsoft.com"
}

$Response = Invoke-RestMethod -Method POST -Uri $authUri -Body $body -ContentType 'application/x-www-form-urlencoded'
$accessToken = $Response.access_token

#Add the authorization header to the request
Write-Verbose 'Adding Authorization headers'

$headers = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
$headers.Add('Authorization', "Bearer $accesstoken")
$headers.Add('Accept', 'application/json')
$headers.Add('Content-Type', 'application/json')
# Needed to filter on specific attributes (https://docs.microsoft.com/en-us/graph/aad-advanced-queries)
$headers.Add('ConsistencyLevel', 'eventual')

Write-Output $headers
}
catch {
throw $_
}
}

function Resolve-HTTPError {
[CmdletBinding()]
param (
[Parameter(Mandatory,
ValueFromPipeline
)]
[object]$ErrorObject
)
process {
$httpErrorObj = [PSCustomObject]@{
FullyQualifiedErrorId = $ErrorObject.FullyQualifiedErrorId
MyCommand = $ErrorObject.InvocationInfo.MyCommand
RequestUri = $ErrorObject.TargetObject.RequestUri
ScriptStackTrace = $ErrorObject.ScriptStackTrace
ErrorMessage = ''
}
if ($ErrorObject.Exception.GetType().FullName -eq 'Microsoft.Powershell.Commands.HttpResponseException') {
$httpErrorObj.ErrorMessage = $ErrorObject.ErrorDetails.Message
}
elseif ($ErrorObject.Exception.GetType().FullName -eq 'System.Net.WebException') {
$httpErrorObj.ErrorMessage = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()).ReadToEnd()
function Convert-StringToBoolean($obj) {
foreach ($property in $obj.PSObject.Properties) {
$value = $property.Value
if ($value -is [string]) {
try {
$obj.$($property.Name) = [System.Convert]::ToBoolean($value)
}
catch {
# Handle cases where conversion fails
$obj.$($property.Name) = $value
}
}
Write-Output $httpErrorObj
}
return $obj
}
#endregion functions

#region Fields to check
$fieldsToCheck = [PSCustomObject]@{
"userPrincipalName" = [PSCustomObject]@{
"userPrincipalName" = [PSCustomObject]@{ # Value returned to HelloID in NonUniqueFields.
accountValue = $actionContext.Data.userPrincipalName
keepInSyncWith = @("mail", "mailNickname") # The properties to keep in sync with, if one of these properties isn't unique, this property wil be treated as not unique as well
crossCheckOn = @("mail") # The properties to keep in cross-check on
keepInSyncWith = @("mail", "mailNickname") # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non-unique.
crossCheckOn = @("mail") # Properties to cross-check for uniqueness.
}
"mail" = [PSCustomObject]@{ # This is the value that is returned to HelloID in NonUniqueFields
"mail" = [PSCustomObject]@{ # Value returned to HelloID in NonUniqueFields.
accountValue = $actionContext.Data.mail
keepInSyncWith = @("userPrincipalName", "mailNickname") # The properties to keep in sync with, if one of these properties isn't unique, this property wil be treated as not unique as well
crossCheckOn = @("userPrincipalName") # The properties to keep in cross-check on
keepInSyncWith = @("userPrincipalName", "mailNickname") # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non-unique.
crossCheckOn = @("userPrincipalName") # Properties to cross-check for uniqueness.
}
"mailNickname" = [PSCustomObject]@{ # This is the value that is returned to HelloID in NonUniqueFields
"mailNickname" = [PSCustomObject]@{ # Value returned to HelloID in NonUniqueFields.
accountValue = $actionContext.Data.mailNickname
keepInSyncWith = @("userPrincipalName", "mail") # The properties to keep in sync with, if one of these properties isn't unique, this property wil be treated as not unique as well
crossCheckOn = $null # The properties to keep in cross-check on
keepInSyncWith = @("userPrincipalName", "mail") # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non-unique.
crossCheckOn = $null # Properties to cross-check for uniqueness.
}
}
#endregion Fields to check

try {
#region Create authorization headers
$actionMessage = "creating authorization headers"

$authorizationHeadersSplatParams = @{
TenantId = $actionContext.Configuration.TenantID
ClientId = $actionContext.Configuration.AppId
ClientSecret = $actionContext.Configuration.AppSecret
#region Create access token
$actionMessage = "creating access token"

$createAccessTokenBody = @{
grant_type = "client_credentials"
client_id = $actionContext.Configuration.AppId
client_secret = $actionContext.Configuration.AppSecret
resource = "https://graph.microsoft.com"
}

$createAccessTokenSplatParams = @{
Uri = "https://login.microsoftonline.com/$($actionContext.Configuration.TenantID)/oauth2/token"
Headers = $headers
Body = $createAccessTokenBody
Method = "POST"
ContentType = "application/x-www-form-urlencoded"
Verbose = $false
ErrorAction = "Stop"
}

$createAccessTokenResonse = Invoke-RestMethod @createAccessTokenSplatParams

Write-Verbose "Created access token. Expires in: $($createAccessTokenResonse.expires_in | ConvertTo-Json)"
#endregion Create access token

#region Create headers
$actionMessage = "creating headers"

$headers = @{
"Accept" = "application/json"
"Content-Type" = "application/json;charset=utf-8"
"Mwp-Api-Version" = "1.0"
}

Write-Verbose "Created headers. Result (without Authorization): $($headers | ConvertTo-Json)."

$headers = New-AuthorizationHeaders @authorizationHeadersSplatParams

Write-Verbose "Created authorization headers. Result: $($headers | ConvertTo-Json)"
#endregion Create authorization headers
# Add Authorization after printing splat
$headers['Authorization'] = "Bearer $($createAccessTokenResonse.access_token)"
#endregion Create headers

if ($actionContext.Operation.ToLower() -ne "create") {
#region Verify account reference
$actionMessage = "verifying account reference"

if ([string]::IsNullOrEmpty($($actionContext.References.Account))) {
throw "The account reference could not be found"
}
#endregion Verify account reference
}

foreach ($fieldToCheck in $fieldsToCheck.PsObject.Properties | Where-Object { -not[String]::IsNullOrEmpty($_.Value.accountValue) }) {
#region Get Microsoft Entra ID account
# Microsoft docs: https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
$actionMessage = "querying Microsoft Entra ID account where [$($fieldToCheck.Name)] = [$($fieldToCheck.Value.accountValue)]"
#region Get account
# API docs: https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http
$actionMessage = "querying account where [$($fieldToCheck.Name)] = [$($fieldToCheck.Value.accountValue)]"

$baseUri = "https://graph.microsoft.com/"
$filter = "$($fieldToCheck.Name) eq '$($fieldToCheck.Value.accountValue)'"
$filter = "$($fieldToCheck.Name) eq '$($fieldToCheck.Value.accountValue)'"
if (($fieldToCheck.Value.crossCheckOn | Measure-Object).Count -ge 1) {
foreach ($fieldToCrossCheckOn in $fieldToCheck.Value.crossCheckOn) {
$filter = $filter + " OR $($fieldToCrossCheckOn) eq '$($fieldToCheck.Value.accountValue)'"
}
}
$getMicrosoftEntraIDAccountSplatParams = @{
Uri = "$($baseUri)/v1.0/users?`$filter=$($filter)&`$select=id,$($fieldToCheck.Name)"
Headers = $headers

$getEntraIDAccountSplatParams = @{
Uri = "https://graph.microsoft.com/v1.0/users?`$filter=$($filter)&`$select=id,$($fieldToCheck.Name)"
Method = "GET"
Verbose = $false
ErrorAction = "Stop"
}
$currentMicrosoftEntraIDAccount = $null
$currentMicrosoftEntraIDAccount = (Invoke-RestMethod @getMicrosoftEntraIDAccountSplatParams).Value

Write-Verbose "Queried Microsoft Entra ID account where [$($fieldToCheck.Name)] = [$($fieldToCheck.Value.accountValue)]. Result: $($currentMicrosoftEntraIDAccount | ConvertTo-Json)."
#endregion Get Microsoft Entra ID account
Write-Verbose "SplatParams: $($getEntraIDAccountSplatParams | ConvertTo-Json)"

# Add header after printing splat
$getEntraIDAccountSplatParams['Headers'] = $headers

$getEntraIDAccountResponse = $null
$getEntraIDAccountResponse = Invoke-RestMethod @getEntraIDAccountSplatParams
$correlatedAccount = $getEntraIDAccountResponse.Value

Write-Verbose "Queried account where [$($fieldToCheck.Name)] = [$($fieldToCheck.Value.accountValue)]. Result: $($correlatedAccount | ConvertTo-Json)"
#endregion Get account

#region Check property uniqueness
$actionMessage = "checking if property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is unique in Microsoft Entra ID"
if (($currentMicrosoftEntraIDAccount | Measure-Object).count -gt 0) {
if ($actionContext.Operation.ToLower() -ne "create" -and $currentMicrosoftEntraIDAccount.id -eq $actionContext.References.Account) {
$actionMessage = "checking if property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is unique"
if (($correlatedAccount | Measure-Object).count -gt 0) {
if ($actionContext.Operation.ToLower() -ne "create" -and $correlatedAccount.id -eq $actionContext.References.Account) {
Write-Verbose "Person is using property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] themselves."
}
else {
Write-Verbose "Property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is not unique in Microsoft Entra ID."
Write-Verbose "In use by: $($currentMicrosoftEntraIDAccount | ConvertTo-Json)."
Write-Verbose "Property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is not unique. In use by account with ID: $($correlatedAccount.id)"
[void]$outputContext.NonUniqueFields.Add($fieldToCheck.Name)

if (($fieldToCheck.Value.keepInSyncWith | Measure-Object).Count -ge 1) {
foreach ($fieldToKeepInSyncWith in $fieldToCheck.Value.keepInSyncWith | Where-Object { $_ -in $actionContext.Data.PsObject.Properties }) {
foreach ($fieldToKeepInSyncWith in $fieldToCheck.Value.keepInSyncWith | Where-Object { $_ -in $actionContext.Data.PsObject.Properties.Name }) {
[void]$outputContext.NonUniqueFields.Add($fieldToKeepInSyncWith)
}
}
}
}
elseif (($currentMicrosoftEntraIDAccount | Measure-Object).count -eq 0) {
Write-Verbose "Property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is unique in Microsoft Entra ID."
elseif (($correlatedAccount | Measure-Object).count -eq 0) {
Write-Verbose "Property [$($fieldToCheck.Name)] with value [$($fieldToCheck.Value.accountValue)] is unique."
}
#endregion Check property uniqueness
}
Expand All @@ -274,16 +219,18 @@ catch {
$($ex.Exception.GetType().FullName -eq 'System.Net.WebException')) {
$errorObj = Resolve-MicrosoftGraphAPIError -ErrorObject $ex
$auditMessage = "Error $($actionMessage). Error: $($errorObj.FriendlyMessage)"
Write-Warning "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)"
$warningMessage = "Error at Line [$($errorObj.ScriptLineNumber)]: $($errorObj.Line). Error: $($errorObj.ErrorDetails)"
}
else {
$auditMessage = "Error $($actionMessage). Error: $($ex.Exception.Message)"
Write-Warning "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)"
$warningMessage = "Error at Line [$($ex.InvocationInfo.ScriptLineNumber)]: $($ex.InvocationInfo.Line). Error: $($ex.Exception.Message)"
}

# Set Success to false
$outputContext.Success = $false

Write-Warning $warningMessage

# Required to write an error as uniqueness check doesn't show auditlog
Write-Error $auditMessage
}
}

0 comments on commit 0574a8c

Please sign in to comment.