33# Check if fields are unique
44# PowerShell V2
55# ################################################
6+
67# Enable TLS1.2
78[System.Net.ServicePointManager ]::SecurityProtocol = [System.Net.ServicePointManager ]::SecurityProtocol -bor [System.Net.SecurityProtocolType ]::Tls12
89
@@ -15,38 +16,12 @@ $InformationPreference = "Continue"
1516$WarningPreference = " Continue"
1617
1718# region functions
18- function Convert-StringToBoolean ($obj ) {
19- if ($obj -is [PSCustomObject ]) {
20- foreach ($property in $obj.PSObject.Properties ) {
21- $value = $property.Value
22- if ($value -is [string ]) {
23- $lowercaseValue = $value.ToLower ()
24- if ($lowercaseValue -eq " true" ) {
25- $obj .$ ($property.Name ) = $true
26- }
27- elseif ($lowercaseValue -eq " false" ) {
28- $obj .$ ($property.Name ) = $false
29- }
30- }
31- elseif ($value -is [PSCustomObject ] -or $value -is [System.Collections.IDictionary ]) {
32- $obj .$ ($property.Name ) = Convert-StringToBoolean $value
33- }
34- elseif ($value -is [System.Collections.IList ]) {
35- for ($i = 0 ; $i -lt $value.Count ; $i ++ ) {
36- $value [$i ] = Convert-StringToBoolean $value [$i ]
37- }
38- $obj .$ ($property.Name ) = $value
39- }
40- }
41- }
42- return $obj
43- }
44-
4519function Resolve-MicrosoftGraphAPIError {
4620 [CmdletBinding ()]
4721 param (
4822 [Parameter (Mandatory )]
49- [object ] $ErrorObject
23+ [object ]
24+ $ErrorObject
5025 )
5126 process {
5227 $httpErrorObj = [PSCustomObject ]@ {
@@ -55,36 +30,33 @@ function Resolve-MicrosoftGraphAPIError {
5530 ErrorDetails = $ErrorObject.Exception.Message
5631 FriendlyMessage = $ErrorObject.Exception.Message
5732 }
58-
5933 if (-not [string ]::IsNullOrEmpty($ErrorObject.ErrorDetails.Message )) {
6034 $httpErrorObj.ErrorDetails = $ErrorObject.ErrorDetails.Message
6135 }
62- elseif ($ErrorObject.Exception -is [System.Net.WebException ] -and $ErrorObject.Exception.Response ) {
63- $streamReaderResponse = [System.IO.StreamReader ]::new($ErrorObject.Exception.Response.GetResponseStream ()).ReadToEnd()
64- if (-not [string ]::IsNullOrEmpty($streamReaderResponse )) {
65- $httpErrorObj.ErrorDetails = $streamReaderResponse
36+ elseif ($ErrorObject.Exception.GetType ().FullName -eq ' System.Net.WebException' ) {
37+ if ($null -ne $ErrorObject.Exception.Response ) {
38+ $streamReaderResponse = [System.IO.StreamReader ]::new($ErrorObject.Exception.Response.GetResponseStream ()).ReadToEnd()
39+ if (-not [string ]::IsNullOrEmpty($streamReaderResponse )) {
40+ $httpErrorObj.ErrorDetails = $streamReaderResponse
41+ }
6642 }
6743 }
68-
6944 try {
70- $errorObjectConverted = $httpErrorObj .ErrorDetails | ConvertFrom-Json - ErrorAction Stop
45+ $errorObjectConverted = $ErrorObject | ConvertFrom-Json - ErrorAction Stop
7146
72- if ($errorObjectConverted.error_description ) {
47+ if ($null -ne $ errorObjectConverted.error_description ) {
7348 $httpErrorObj.FriendlyMessage = $errorObjectConverted.error_description
7449 }
75- elseif ($errorObjectConverted.error ) {
76- $httpErrorObj.FriendlyMessage = $errorObjectConverted.error.message
77- if ($errorObjectConverted.error.code ) {
78- $httpErrorObj.FriendlyMessage += " Error code: $ ( $errorObjectConverted.error.code ) ."
79- }
80- if ($errorObjectConverted.error.details ) {
81- if ($errorObjectConverted.error.details.message ) {
82- $httpErrorObj.FriendlyMessage += " Details message: $ ( $errorObjectConverted.error.details.message ) "
83- }
84- if ($errorObjectConverted.error.details.code ) {
85- $httpErrorObj.FriendlyMessage += " Details code: $ ( $errorObjectConverted.error.details.code ) ."
50+ elseif ($null -ne $errorObjectConverted.error ) {
51+ if ($null -ne $errorObjectConverted.error.message ) {
52+ $httpErrorObj.FriendlyMessage = $errorObjectConverted.error.message
53+ if ($null -ne $errorObjectConverted.error.code ) {
54+ $httpErrorObj.FriendlyMessage = $httpErrorObj.FriendlyMessage + " Error code: $ ( $errorObjectConverted.error.code ) "
8655 }
8756 }
57+ else {
58+ $httpErrorObj.FriendlyMessage = $errorObjectConverted.error
59+ }
8860 }
8961 else {
9062 $httpErrorObj.FriendlyMessage = $ErrorObject
@@ -93,174 +65,147 @@ function Resolve-MicrosoftGraphAPIError {
9365 catch {
9466 $httpErrorObj.FriendlyMessage = $httpErrorObj.ErrorDetails
9567 }
96-
9768 Write-Output $httpErrorObj
9869 }
9970}
10071
101- function New-AuthorizationHeaders {
102- [CmdletBinding ()]
103- [OutputType ([System.Collections.Generic.Dictionary [[String ], [String ]]])]
104- param (
105- [parameter (Mandatory )]
106- [string ]
107- $TenantId ,
108-
109- [parameter (Mandatory )]
110- [string ]
111- $ClientId ,
112-
113- [parameter (Mandatory )]
114- [string ]
115- $ClientSecret
116- )
117- try {
118- Write-Verbose " Creating Access Token"
119- $baseUri = " https://login.microsoftonline.com/"
120- $authUri = $baseUri + " $TenantId /oauth2/token"
121-
122- $body = @ {
123- grant_type = " client_credentials"
124- client_id = " $ClientId "
125- client_secret = " $ClientSecret "
126- resource = " https://graph.microsoft.com"
127- }
128-
129- $Response = Invoke-RestMethod - Method POST - Uri $authUri - Body $body - ContentType ' application/x-www-form-urlencoded'
130- $accessToken = $Response.access_token
131-
132- # Add the authorization header to the request
133- Write-Verbose ' Adding Authorization headers'
134-
135- $headers = [System.Collections.Generic.Dictionary [[String ], [String ]]]::new()
136- $headers.Add (' Authorization' , " Bearer $accesstoken " )
137- $headers.Add (' Accept' , ' application/json' )
138- $headers.Add (' Content-Type' , ' application/json' )
139- # Needed to filter on specific attributes (https://docs.microsoft.com/en-us/graph/aad-advanced-queries)
140- $headers.Add (' ConsistencyLevel' , ' eventual' )
141-
142- Write-Output $headers
143- }
144- catch {
145- throw $_
146- }
147- }
148-
149- function Resolve-HTTPError {
150- [CmdletBinding ()]
151- param (
152- [Parameter (Mandatory ,
153- ValueFromPipeline
154- )]
155- [object ]$ErrorObject
156- )
157- process {
158- $httpErrorObj = [PSCustomObject ]@ {
159- FullyQualifiedErrorId = $ErrorObject.FullyQualifiedErrorId
160- MyCommand = $ErrorObject.InvocationInfo.MyCommand
161- RequestUri = $ErrorObject.TargetObject.RequestUri
162- ScriptStackTrace = $ErrorObject.ScriptStackTrace
163- ErrorMessage = ' '
164- }
165- if ($ErrorObject.Exception.GetType ().FullName -eq ' Microsoft.Powershell.Commands.HttpResponseException' ) {
166- $httpErrorObj.ErrorMessage = $ErrorObject.ErrorDetails.Message
167- }
168- elseif ($ErrorObject.Exception.GetType ().FullName -eq ' System.Net.WebException' ) {
169- $httpErrorObj.ErrorMessage = [System.IO.StreamReader ]::new($ErrorObject.Exception.Response.GetResponseStream ()).ReadToEnd()
72+ function Convert-StringToBoolean ($obj ) {
73+ foreach ($property in $obj.PSObject.Properties ) {
74+ $value = $property.Value
75+ if ($value -is [string ]) {
76+ try {
77+ $obj .$ ($property.Name ) = [System.Convert ]::ToBoolean($value )
78+ }
79+ catch {
80+ # Handle cases where conversion fails
81+ $obj .$ ($property.Name ) = $value
82+ }
17083 }
171- Write-Output $httpErrorObj
17284 }
85+ return $obj
17386}
17487# endregion functions
17588
17689# region Fields to check
17790$fieldsToCheck = [PSCustomObject ]@ {
178- " userPrincipalName" = [PSCustomObject ]@ {
91+ " userPrincipalName" = [PSCustomObject ]@ { # Value returned to HelloID in NonUniqueFields.
17992 accountValue = $actionContext.Data.userPrincipalName
180- 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
181- crossCheckOn = @ (" mail" ) # The properties to keep in cross-check on
93+ keepInSyncWith = @ (" mail" , " mailNickname" ) # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non- unique.
94+ crossCheckOn = @ (" mail" ) # Properties to cross-check for uniqueness.
18295 }
183- " mail" = [PSCustomObject ]@ { # This is the value that is returned to HelloID in NonUniqueFields
96+ " mail" = [PSCustomObject ]@ { # Value returned to HelloID in NonUniqueFields.
18497 accountValue = $actionContext.Data.mail
185- 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
186- crossCheckOn = @ (" userPrincipalName" ) # The properties to keep in cross-check on
98+ keepInSyncWith = @ (" userPrincipalName" , " mailNickname" ) # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non- unique.
99+ crossCheckOn = @ (" userPrincipalName" ) # Properties to cross-check for uniqueness.
187100 }
188- " mailNickname" = [PSCustomObject ]@ { # This is the value that is returned to HelloID in NonUniqueFields
101+ " mailNickname" = [PSCustomObject ]@ { # Value returned to HelloID in NonUniqueFields.
189102 accountValue = $actionContext.Data.mailNickname
190- 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
191- crossCheckOn = $null # The properties to keep in cross-check on
103+ keepInSyncWith = @ (" userPrincipalName" , " mail" ) # Properties to synchronize with. If any of these properties are not unique, this property will also be treated as non- unique.
104+ crossCheckOn = $null # Properties to cross-check for uniqueness.
192105 }
193106}
194107# endregion Fields to check
195108
196109try {
197- # region Create authorization headers
198- $actionMessage = " creating authorization headers"
199-
200- $authorizationHeadersSplatParams = @ {
201- TenantId = $actionContext.Configuration.TenantID
202- ClientId = $actionContext.Configuration.AppId
203- ClientSecret = $actionContext.Configuration.AppSecret
110+ # region Create access token
111+ $actionMessage = " creating access token"
112+
113+ $createAccessTokenBody = @ {
114+ grant_type = " client_credentials"
115+ client_id = $actionContext.Configuration.AppId
116+ client_secret = $actionContext.Configuration.AppSecret
117+ resource = " https://graph.microsoft.com"
204118 }
119+
120+ $createAccessTokenSplatParams = @ {
121+ Uri = " https://login.microsoftonline.com/$ ( $actionContext.Configuration.TenantID ) /oauth2/token"
122+ Headers = $headers
123+ Body = $createAccessTokenBody
124+ Method = " POST"
125+ ContentType = " application/x-www-form-urlencoded"
126+ Verbose = $false
127+ ErrorAction = " Stop"
128+ }
129+
130+ $createAccessTokenResonse = Invoke-RestMethod @createAccessTokenSplatParams
131+
132+ Write-Verbose " Created access token. Expires in: $ ( $createAccessTokenResonse.expires_in | ConvertTo-Json ) "
133+ # endregion Create access token
134+
135+ # region Create headers
136+ $actionMessage = " creating headers"
137+
138+ $headers = @ {
139+ " Accept" = " application/json"
140+ " Content-Type" = " application/json;charset=utf-8"
141+ " Mwp-Api-Version" = " 1.0"
142+ }
143+
144+ Write-Verbose " Created headers. Result (without Authorization): $ ( $headers | ConvertTo-Json ) ."
205145
206- $headers = New-AuthorizationHeaders @authorizationHeadersSplatParams
207-
208- Write-Verbose " Created authorization headers. Result: $ ( $headers | ConvertTo-Json ) "
209- # endregion Create authorization headers
146+ # Add Authorization after printing splat
147+ $headers [' Authorization' ] = " Bearer $ ( $createAccessTokenResonse.access_token ) "
148+ # endregion Create headers
210149
211150 if ($actionContext.Operation.ToLower () -ne " create" ) {
212151 # region Verify account reference
213152 $actionMessage = " verifying account reference"
153+
214154 if ([string ]::IsNullOrEmpty($ ($actionContext.References.Account ))) {
215155 throw " The account reference could not be found"
216156 }
217157 # endregion Verify account reference
218158 }
159+
219160 foreach ($fieldToCheck in $fieldsToCheck.PsObject.Properties | Where-Object { -not [String ]::IsNullOrEmpty($_.Value.accountValue ) }) {
220- # region Get Microsoft Entra ID account
221- # Microsoft docs: https://learn.microsoft.com/en-us/graph/api/user-get ?view=graph-rest-1.0&tabs=http
222- $actionMessage = " querying Microsoft Entra ID account where [$ ( $fieldToCheck.Name ) ] = [$ ( $fieldToCheck.Value.accountValue ) ]"
161+ # region Get account
162+ # API docs: https://learn.microsoft.com/en-us/graph/api/user-list ?view=graph-rest-1.0&tabs=http
163+ $actionMessage = " querying account where [$ ( $fieldToCheck.Name ) ] = [$ ( $fieldToCheck.Value.accountValue ) ]"
223164
224- $baseUri = " https://graph.microsoft.com/"
225- $filter = " $ ( $fieldToCheck.Name ) eq '$ ( $fieldToCheck.Value.accountValue ) '"
165+ $filter = " $ ( $fieldToCheck.Name ) eq '$ ( $fieldToCheck.Value.accountValue ) '"
226166 if (($fieldToCheck.Value.crossCheckOn | Measure-Object ).Count -ge 1 ) {
227167 foreach ($fieldToCrossCheckOn in $fieldToCheck.Value.crossCheckOn ) {
228168 $filter = $filter + " OR $ ( $fieldToCrossCheckOn ) eq '$ ( $fieldToCheck.Value.accountValue ) '"
229169 }
230170 }
231- $getMicrosoftEntraIDAccountSplatParams = @ {
232- Uri = " $ ( $baseUri ) /v1.0/users? `$ filter= $ ( $filter ) & `$ select=id, $ ( $fieldToCheck .Name ) "
233- Headers = $headers
171+
172+ $getEntraIDAccountSplatParams = @ {
173+ Uri = " https://graph.microsoft.com/v1.0/users? `$ filter= $ ( $filter ) & `$ select=id, $ ( $fieldToCheck .Name ) "
234174 Method = " GET"
235175 Verbose = $false
236176 ErrorAction = " Stop"
237177 }
238- $currentMicrosoftEntraIDAccount = $null
239- $currentMicrosoftEntraIDAccount = (Invoke-RestMethod @getMicrosoftEntraIDAccountSplatParams ).Value
240178
241- Write-Verbose " Queried Microsoft Entra ID account where [$ ( $fieldToCheck.Name ) ] = [$ ( $fieldToCheck.Value.accountValue ) ]. Result: $ ( $currentMicrosoftEntraIDAccount | ConvertTo-Json ) ."
242- # endregion Get Microsoft Entra ID account
179+ Write-Verbose " SplatParams: $ ( $getEntraIDAccountSplatParams | ConvertTo-Json ) "
180+
181+ # Add header after printing splat
182+ $getEntraIDAccountSplatParams [' Headers' ] = $headers
183+
184+ $getEntraIDAccountResponse = $null
185+ $getEntraIDAccountResponse = Invoke-RestMethod @getEntraIDAccountSplatParams
186+ $correlatedAccount = $getEntraIDAccountResponse.Value
187+
188+ Write-Verbose " Queried account where [$ ( $fieldToCheck.Name ) ] = [$ ( $fieldToCheck.Value.accountValue ) ]. Result: $ ( $correlatedAccount | ConvertTo-Json ) "
189+ # endregion Get account
243190
244191 # region Check property uniqueness
245- $actionMessage = " checking if property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is unique in Microsoft Entra ID "
246- if (($currentMicrosoftEntraIDAccount | Measure-Object ).count -gt 0 ) {
247- if ($actionContext.Operation.ToLower () -ne " create" -and $currentMicrosoftEntraIDAccount .id -eq $actionContext.References.Account ) {
192+ $actionMessage = " checking if property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is unique"
193+ if (($correlatedAccount | Measure-Object ).count -gt 0 ) {
194+ if ($actionContext.Operation.ToLower () -ne " create" -and $correlatedAccount .id -eq $actionContext.References.Account ) {
248195 Write-Verbose " Person is using property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] themselves."
249196 }
250197 else {
251- Write-Verbose " Property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is not unique in Microsoft Entra ID."
252- Write-Verbose " In use by: $ ( $currentMicrosoftEntraIDAccount | ConvertTo-Json ) ."
198+ Write-Verbose " Property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is not unique. In use by account with ID: $ ( $correlatedAccount.id ) "
253199 [void ]$outputContext.NonUniqueFields.Add ($fieldToCheck.Name )
254-
255200 if (($fieldToCheck.Value.keepInSyncWith | Measure-Object ).Count -ge 1 ) {
256- foreach ($fieldToKeepInSyncWith in $fieldToCheck.Value.keepInSyncWith | Where-Object { $_ -in $actionContext.Data.PsObject.Properties }) {
201+ foreach ($fieldToKeepInSyncWith in $fieldToCheck.Value.keepInSyncWith | Where-Object { $_ -in $actionContext.Data.PsObject.Properties.Name }) {
257202 [void ]$outputContext.NonUniqueFields.Add ($fieldToKeepInSyncWith )
258203 }
259204 }
260205 }
261206 }
262- elseif (($currentMicrosoftEntraIDAccount | Measure-Object ).count -eq 0 ) {
263- Write-Verbose " Property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is unique in Microsoft Entra ID ."
207+ elseif (($correlatedAccount | Measure-Object ).count -eq 0 ) {
208+ Write-Verbose " Property [$ ( $fieldToCheck.Name ) ] with value [$ ( $fieldToCheck.Value.accountValue ) ] is unique."
264209 }
265210 # endregion Check property uniqueness
266211 }
@@ -274,16 +219,18 @@ catch {
274219 $ ($ex.Exception.GetType ().FullName -eq ' System.Net.WebException' )) {
275220 $errorObj = Resolve-MicrosoftGraphAPIError - ErrorObject $ex
276221 $auditMessage = " Error $ ( $actionMessage ) . Error: $ ( $errorObj.FriendlyMessage ) "
277- Write-Warning " Error at Line [$ ( $errorObj.ScriptLineNumber ) ]: $ ( $errorObj.Line ) . Error: $ ( $errorObj.ErrorDetails ) "
222+ $warningMessage = " Error at Line [$ ( $errorObj.ScriptLineNumber ) ]: $ ( $errorObj.Line ) . Error: $ ( $errorObj.ErrorDetails ) "
278223 }
279224 else {
280225 $auditMessage = " Error $ ( $actionMessage ) . Error: $ ( $ex.Exception.Message ) "
281- Write-Warning " Error at Line [$ ( $ex.InvocationInfo.ScriptLineNumber ) ]: $ ( $ex.InvocationInfo.Line ) . Error: $ ( $ex.Exception.Message ) "
226+ $warningMessage = " Error at Line [$ ( $ex.InvocationInfo.ScriptLineNumber ) ]: $ ( $ex.InvocationInfo.Line ) . Error: $ ( $ex.Exception.Message ) "
282227 }
283228
284229 # Set Success to false
285230 $outputContext.Success = $false
286231
232+ Write-Warning $warningMessage
233+
287234 # Required to write an error as uniqueness check doesn't show auditlog
288235 Write-Error $auditMessage
289- }
236+ }
0 commit comments