Providers/Entra/Public/New-EntraDevice.ps1
|
function New-EntraDevice { <# .EXTERNALHELP TestEnvironment-Help.xml .SYNOPSIS Creates the seeded device objects defined in Data\EntraDevices.csv #> [CmdletBinding(SupportsShouldProcess)] [OutputType('EntraDevice')] param( [Parameter()] [string[]]$DeviceKey, [Parameter()] [ValidateSet('Core', 'Bulk')] [string[]]$Tier, [Parameter()] [switch]$SkipOwners, [Parameter()] [switch]$ShowProgress, [Parameter()] [switch]$PassThru ) $connection = Get-EntraConnection $marker = Get-EntraSeedMarker -Connection $connection $definitions = @(Get-EntraSeedData -Name 'EntraDevices') if ($Tier) { $definitions = @($definitions | Where-Object { $Tier -contains $_.Tier }) } if ($DeviceKey) { $definitions = @($definitions | Where-Object { $DeviceKey -contains $_.Key }) $missing = @($DeviceKey | Where-Object { $definitions.Key -notcontains $_ }) if ($missing) { Write-Error "No seed definition for device key(s): $($missing -join ', ')" -ErrorAction Stop return } } Write-Verbose "Creating $($definitions.Count) device(s)" # --- Phase 1: create --------------------------------------------------------------- $createRequests = [System.Collections.Generic.List[object]]::new() $definitionByKey = @{} foreach ($definition in $definitions) { $displayName = '{0}{1}' -f $marker.Prefix, $definition.DisplayName $definitionByKey[$definition.Key] = $definition if (-not $PSCmdlet.ShouldProcess($displayName, 'Create device')) { continue } # The alternative security identifier is required and its key must be base64. The # content is arbitrary for a device that will never authenticate, so it carries the # seed marker - not a proof of ownership, since Graph will not filter on it, but it # does let an operator inspecting the raw object see where it came from. $securityIdentity = [Convert]::ToBase64String( [System.Text.Encoding]::UTF8.GetBytes("$($marker.Tag):$($definition.Key)")) $createRequests.Add([PSCustomObject]@{ Reference = $definition.Key Method = 'POST' Url = '/devices' Body = @{ displayName = $displayName deviceId = [guid]::NewGuid().ToString() operatingSystem = $definition.OperatingSystem operatingSystemVersion = $definition.OperatingSystemVersion accountEnabled = [bool]::Parse($definition.AccountEnabled) isCompliant = [bool]::Parse($definition.IsCompliant) isManaged = [bool]::Parse($definition.IsManaged) alternativeSecurityIds = @(@{ type = 2; key = $securityIdentity }) } }) } if ($createRequests.Count -eq 0) { Write-TestProgress -Activity 'Seeding devices' -Completed -ShowProgress:$ShowProgress return } $createResults = @(Invoke-EntraBatch -Request $createRequests.ToArray() -Connection $connection ` -Activity 'Seeding devices' -ShowProgress:$ShowProgress) $idByKey = @{} foreach ($result in $createResults) { if ($result.Success -and $result.Body.id) { $idByKey[$result.Reference] = $result.Body.id } else { Write-Warning "Could not create device '$($result.Reference)': $($result.Error)" } } Write-Verbose "Created $($idByKey.Count) of $($createRequests.Count) device(s)" # --- Phase 2: registered owners ---------------------------------------------------- if (-not $SkipOwners -and $idByKey.Count -gt 0) { $userIdByKey = @{} foreach ($user in (Get-EntraSeededObject -Type Users -Connection $connection)) { if ($user.userPrincipalName -match "^$([regex]::Escape($marker.Prefix))(?<key>.+?)@") { $userIdByKey[$Matches['key']] = $user.id } } $ownerRequests = [System.Collections.Generic.List[object]]::new() $missingOwners = 0 foreach ($key in $idByKey.Keys) { $owner = $definitionByKey[$key].RegisteredOwner if (-not $owner) { continue } if (-not $userIdByKey.ContainsKey($owner)) { $missingOwners++; continue } $ownerRequests.Add([PSCustomObject]@{ Reference = $key Method = 'POST' Url = "/devices/$($idByKey[$key])/registeredOwners/`$ref" Body = @{ '@odata.id' = "$($connection.GraphBaseUri)/v1.0/directoryObjects/$($userIdByKey[$owner])" } }) } if ($missingOwners -gt 0) { Write-Warning "$missingOwners device(s) named an owner that does not exist and were left unowned." } if ($ownerRequests.Count -gt 0) { # -RetryOnNotFound because a device is reliably not addressable for its owner # reference for a few seconds after the create returns. $ownerResults = @(Invoke-EntraBatch -Request $ownerRequests.ToArray() -Connection $connection ` -RetryOnNotFound -Activity 'Assigning device owners' -ShowProgress:$ShowProgress) $duplicate = 'already exist|added object references already exist|A conflicting object' $failed = @($ownerResults | Where-Object { -not $_.Success -and $_.Error -notmatch $duplicate }) if ($failed.Count -gt 0) { Write-Warning "$($failed.Count) device owner(s) could not be set. First error: $($failed[0].Error)" } Write-Verbose "Set $(@($ownerResults | Where-Object Success).Count) device owner(s)" } } # --- Phase 3: containment ---------------------------------------------------------- if ($idByKey.Count -gt 0) { $unit = @(Get-EntraSeededObject -Type AdministrativeUnits -Connection $connection) | Where-Object { $_.displayName -eq ('{0}Devices' -f $marker.Prefix) } | Select-Object -First 1 if ($unit) { $placed = Add-EntraUnitMember -UnitId $unit.id -ObjectId @($idByKey.Values) -Connection $connection ` -Activity 'Placing devices in their administrative unit' -ShowProgress:$ShowProgress Write-Verbose "Placed $placed device(s) in '$($unit.displayName)'" } else { Write-Warning ("No $($marker.Prefix)Devices administrative unit exists, so the created devices are not " + "contained. Run New-EntraAdministrativeUnit first.") } } Write-TestProgress -Activity 'Seeding devices' -Completed -ShowProgress:$ShowProgress if ($PassThru) { return @(foreach ($key in $idByKey.Keys) { $definition = $definitionByKey[$key] [PSCustomObject]@{ PSTypeName = 'EntraDevice' Key = $key Id = $idByKey[$key] DisplayName = '{0}{1}' -f $marker.Prefix, $definition.DisplayName OperatingSystem = $definition.OperatingSystem IsCompliant = [bool]::Parse($definition.IsCompliant) IsManaged = [bool]::Parse($definition.IsManaged) AccountEnabled = [bool]::Parse($definition.AccountEnabled) RegisteredOwner = $definition.RegisteredOwner Tier = $definition.Tier Purpose = $definition.Purpose } }) } } |