functions/azure/aad/Assert-AzureAdSecurityGroup.ps1
# <copyright file="Assert-AzureAdSecurityGroup.ps1" company="Endjin Limited"> # Copyright (c) Endjin Limited. All rights reserved. # </copyright> <# .SYNOPSIS Creates or updates a AzureAD group. .DESCRIPTION Uses Azure PowerShell to create an AzureAD security group. This function assumes that the caller will not have full AzureAD permissions (i.e. 'Group.Create' instead of 'Group.ReadWrite.All') as this is typically the case for least-privilege automation scenarios. It therefore assumes that group owners can only be configured as part of the creation request, as this is supported for callers with 'Group.Create' permissions. .PARAMETER DisplayName The display name of the group. .PARAMETER MailNickname The username portion of the email address associated with the group .PARAMETER Description The description of the group .PARAMETER OwnersToAssignOnCreation The DisplayName, UserPrincipalName, ObjectId or ApplicationId of the users, groups, service principals to assign as owners to the group. Note, that if the group already exists, we will not attempt to assign the owners (see the note in the description for more details) .PARAMETER StrictMode When true, the group's description forms part of the idempotency check. If the specified description does not match the group's definition in AzureAD, then it will be updated to ensure it matches. .OUTPUTS AzureAD group definition object #> function Assert-AzureAdSecurityGroup { [CmdletBinding()] param ( [Alias("Name")] [Parameter(Mandatory=$true)] [string] $DisplayName, [Alias("EmailName")] [Parameter(Mandatory=$true)] [string] $MailNickname, [Parameter()] [string] $Description, [Parameter()] [string[]] $OwnersToAssignOnCreation, [Parameter()] [bool] $StrictMode = $true # default to true, to ensure backwards-compatible behaviour ) # Check whether we have a valid AzPowerShell connection, but no subscription-level access is required _EnsureAzureConnection -AzPowerShell -TenantOnly -ErrorAction Stop | Out-Null $existingGroup = Get-AzADGroup -DisplayName $DisplayName # Resolve the ObjectId for any specified owners [array]$ownersToAssignObjectIds = $OwnersToAssignOnCreation | Where-Object { $_ } | ForEach-Object { Get-AzureAdDirectoryObject -Criterion $_ } if ($existingGroup) { Write-Host "Security group with name $($existingGroup.displayName) already exists." if ($ownersToAssignObjectIds) { [array]$existingOwners = _getGroupOwners -GroupObjectId $existingGroup.id $ownersToAssignObjectIds | ForEach-Object { if ($_.id -notin $existingOwners) { Write-Warning "Object ID '$($_.id)' was specified to be assigned as group owner, but group already exists and the ownership cannot be updated." } } } $requestParams = _buildUpdateRequest if ($requestParams) { Write-Host "Security group needs to be updated..." } } else { Write-Host "Security group with name $DisplayName doesn't exist. Creating..." $requestParams = _buildCreateRequest -OwnersObjectIds ($ownersToAssignObjectIds | Select-Object -ExpandProperty id) } if ($requestParams) { $resp = Invoke-AzRestMethod @requestParams if ($resp.StatusCode -ge 400) { $errorMessage = "AAD Security group processing failed: $($resp.Content | ConvertFrom-Json | Select-Object -ExpandProperty error)" throw $errorMessage } Write-Host "AAD Security group processing complete" $existingGroup = Get-AzADGroup -ObjectId ($existingGroup ? $existingGroup.id : ($resp.Content | ConvertFrom-Json).id) } return $existingGroup } function _buildUpdateRequest { if ($StrictMode -and $Description -ine $existingGroup.description -and ![string]::IsNullOrEmpty($Description)) { Write-Host "Description field has changed. Updating..." $updateBody = @{ displayName = $existingGroup.displayName mailNickname = $existingGroup.mailNickname mailEnabled = $existingGroup.mailEnabled securityEnabled = $existingGroup.securityEnabled description = $Description } $restParams = @{ Uri = "https://graph.microsoft.com/v1.0/groups/$($existingGroup.Id)" Method = "PATCH" Payload = ($updateBody | ConvertTo-Json -Depth 3 -Compress) } return $restParams } else { return $null } } function _buildCreateRequest { param ( [guid[]] $OwnersObjectIds ) $body = @{ displayName = $DisplayName mailNickname = $MailNickname mailEnabled = $false securityEnabled = $true } if ($OwnersObjectIds) { $body["owners@odata.bind"] = @() $body["owners@odata.bind"] += ($OwnersObjectIds | Where-Object { $_ } | ForEach-Object { "https://graph.microsoft.com/v1.0/directoryObjects/$($_.ToString())" }) } if ($Description) { $body["description"] = $Description } $restParams = @{ Uri = "https://graph.microsoft.com/v1.0/groups" Method = "POST" Payload = ($body | ConvertTo-Json -Depth 3 -Compress) } return $restParams } function _getGroupOwners { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [guid] $GroupObjectId ) # May-2022: Workaround a current limitation, whereby service principal group owners are not returned by the v1.0 MS Graph API # ref: https://docs.microsoft.com/en-us/graph/api/group-list-owners?view=graph-rest-1.0&tabs=http $resp = Invoke-AzRestMethod -Method GET -Uri "https://graph.microsoft.com/beta/groups/$($GroupObjectId.Guid)/owners" if ($resp.StatusCode -ge 400) { $errorMessage = "Failed to lookup existing group owners [ObjectId=$($GroupObjectId.Guid)]: $($resp.Content | ConvertFrom-Json | Select-Object -ExpandProperty error)" throw $errorMessage } $ownerObjectIds = $resp | Select-Object -ExpandProperty Content | ConvertFrom-Json | Select-Object -ExpandProperty value | Select-Object -ExpandProperty id return $ownerObjectIds } |