Manage Azure AD groups using the Graph API with PowerShell

Managing Azure AD groups is a dependency for the Conditional Access policies, as for (almost) every policy I’ll be creating both an inclusion and exclusion group. I’ll also be creating nested groups, such as dynamic groups such as “All Users” and “All Guests”.

This creates a complete solution that can be deployed in an Azure Pipeline.

Managing Azure AD groups

Get an Azure AD group

The first function is Get-WTAzureADGroup, which you can access from my GitHub.

This gets the Azure AD groups, including all, specific IDs and specific group properties. This is needed in order to compare what’s in Azure AD, to what may need to be updated or removed within the pipeline.

Examples below:

Expand code block
# Clone repo that contains the Graph API and ToolKit functions
git clone --branch main --single-branch https://github.com/wesley-trust/GraphAPI.git
git clone --branch main --single-branch https://github.com/wesley-trust/ToolKit.git

# Dot source function into memory
. .\GraphAPI\Public\AzureAD\Groups\Get-WTAzureADGroup.ps1

# Define Variables
$ClientID = "sdg23497-sd82-983s-sdf23-dsf234kafs24"
$ClientSecret = "khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"
$TenantDomain = "wesleytrustsandbox.onmicrosoft.com"
$IDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"

# Create hashtable
$ServicePrincipal = @{
  ClientID     = $ClientID
  ClientSecret = $ClientSecret
  TenantDomain = $TenantDomain
}

# Get all groups, splat the hashtable containing the service principal to obtain an access token
Get-WTAzureADGroup @ServicePrincipal

# Or pipe specific IDs to get to the function, splat the hashtable containing the service principal
$IDs | Get-WTAzureADGroup @ServicePrincipal

# Or specify each parameter individually, including an access token previously obtained
Get-WTAzureADGroup -AccessToken $AccessToken -IDs $IDs

What does this do?

  • This sets specific variables, including the activity, the tags to be evaluated against the groups, and the Graph Uri
  • An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
  • A set of group properties are returned by default, but a select query to return just specific properties is included
    • I tidy this up removing spaces, as well as adding the requirement of id and displayName for tagging
  • The private function is then called, with the query altered as appropriate depending on the parameters

The complete function as at this date, is below:

Expand code block (always grab the latest version from GitHub)
function Get-WTAzureADGroup {
    [cmdletbinding()]
    param (
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client ID for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientID,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client secret for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientSecret,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The initial domain (onmicrosoft.com) of the tenant"
        )]
        [string]$TenantDomain,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The access token, obtained from executing Get-WTGraphAccessToken"
        )]
        [string]$AccessToken,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude features in preview, a production API version will be used instead"
        )]
        [switch]$ExcludePreviewFeatures,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude tag processing of groups"
        )]
        [switch]$ExcludeTagEvaluation,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            ValueFromPipeLine = $true,
            HelpMessage = "The Azure AD groups to get, this must contain valid id(s)"
        )]
        [Alias("id", "GroupID", "GroupIDs")]
        [string[]]$IDs,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Comma separated list of properties, 'id' is always selected, 'displayName' will also be selected if tagging is not excluded"
        )]
        [string]$Select
    )
    Begin {
        try {
            # Function definitions
            $Functions = @(
                "GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
                "GraphAPI\Private\Invoke-WTGraphGet.ps1"
            )

            # Function dot source
            foreach ($Function in $Functions) {
                . $Function
            }

            # Variables
            $Activity = "Getting Azure AD groups"
            $Uri = "groups"
            $Tags = @("SVC", "REF", "ENV")

        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    Process {
        try {

            # If there is no access token, obtain one
            if (!$AccessToken) {
                $AccessToken = Get-WTGraphAccessToken `
                    -ClientID $ClientID `
                    -ClientSecret $ClientSecret `
                    -TenantDomain $TenantDomain
            }
            if ($AccessToken) {
                
                # Build Parameters
                $Parameters = @{
                    AccessToken = $AccessToken
                    Activity    = $Activity
                }
                if ($ExcludePreviewFeatures) {
                    $Parameters.Add("ExcludePreviewFeatures", $true)
                }
                if (!$ExcludeTagEvaluation) {
                    $Parameters.Add("Tags", $Tags)
                }

                # If select is specified, a different query should be built
                if ($Select) {
                    
                    # Clean up input to remove remove any spaces
                    $Select = $Select.Replace(" ", "")

                    # Adding 'id' which is required for a result, 'displayName' is also added if tagging is not excluded, as it is a dependency
                    $Select = "id,$Select"
                    if (!$ExcludeTagEvaluation) {
                        $Select = "displayName,$Select"
                    }

                    # If there are Ids, get Azure AD group with selected properties only
                    if ($IDs) {
                        $QueryResponse = foreach ($Id in $IDs) {
                            Invoke-WTGraphGet @Parameters -Uri "$Uri/$($Id)?`$select=$Select"
                        }
                    }
                    else {
                        $WarningMessage = "A select query requires an ID to be specified for the group"
                        Write-Warning $WarningMessage
                    }
                }
                else {
                    if ($IDs) {
                        $Parameters.Add("IDs", $IDs)
                    }

                    # Get Azure AD groups with default properties
                    $QueryResponse = Invoke-WTGraphGet @Parameters -Uri $Uri
                }

                # Return response if one is returned
                if ($QueryResponse) {
                    $QueryResponse
                }
                else {
                    $WarningMessage = "No Azure AD groups exist in Azure AD, or with parameters specified"
                    Write-Warning $WarningMessage
                }
            }
            else {
                $ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
                Write-Error $ErrorMessage
                throw $ErrorMessage
            }
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    End {
        try {
            
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
}

Update an Azure AD group

The next function is Edit-WTAzureADGroup, which you can access from my GitHub.

This performs an edit (update) to the Azure AD groups. This allows changes such as the displayName to be altered for the group within the pipeline, if the config files have been updated with a new name.

Examples below:

Expand code block
# Clone repo that contains the Graph API functions
git clone --branch main --single-branch https://github.com/wesley-trust/GraphAPI.git

# Dot source function into memory
. .\GraphAPI\Public\AzureAD\Groups\Edit-WTAzureADGroup.ps1

# Define Variables
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$Id = "gve33497-hb48-983s-5fg36-dsf234kafs24"
$DisplayName = "SVC-CA; Updated displayName"

# Create input object
$AzureADGroup = [PSCustomObject]@{
  id          = $Id
  displayName = $DisplayName
}

# Pipe the Azure AD group to the function, specify an access token previously obtained
$AzureADGroup | Edit-WTAzureADGroup -AccessToken $AccessToken

# Or specify each parameter individually, including an access token previously obtained
Edit-WTAzureADGroup -AccessToken $AccessToken -AzureADGroup $AzureADGroup

What does this do?

  • This sets specific variables, including the activity and the Graph Uri
    • As well as properties to remove from the input that would cause errors as they are readonly (or not recognised)
    • The properties are cleaned up within the private patch function
  • An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
  • An id is required in order to update a group, but this check is done within the private patch function
  • The private function is then called

The complete function as at this date, is below:

Expand code block (always grab the latest version from GitHub)
function Edit-WTAzureADGroup {
    [cmdletbinding()]
    param (
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client ID for the Azure AD service principal with Azure AD Graph permissions"
        )]
        [string]$ClientID,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client secret for the Azure AD service principal with Azure AD Graph permissions"
        )]
        [string]$ClientSecret,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The initial domain (onmicrosoft.com) of the tenant"
        )]
        [string]$TenantDomain,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The access token, obtained from executing Get-WTGraphAccessToken"
        )]
        [string]$AccessToken,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude features in preview, a production API version will be used instead"
        )]
        [switch]$ExcludePreviewFeatures,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            ValueFromPipeLine = $true,
            HelpMessage = "The Azure AD groups to remove, a group must have a valid id"
        )]
        [Alias('AzureADGroup', 'GroupDefinition')]
        [PSCustomObject]$AzureADGroups
    )
    Begin {
        try {
            # Function definitions
            $Functions = @(
                "GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
                "GraphAPI\Private\Invoke-WTGraphPatch.ps1"
            )

            # Function dot source
            foreach ($Function in $Functions) {
                . $Function
            }

            # Variables
            $Activity = "Updating Azure AD Groups"
            $Uri = "groups"
            $CleanUpProperties = (
                "createdDateTime",
                "modifiedDateTime",
                "SideIndicator"
            )

        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    Process {
        try {

            # If there is no access token, obtain one
            if (!$AccessToken) {
                $AccessToken = Get-WTGraphAccessToken `
                    -ClientID $ClientID `
                    -ClientSecret $ClientSecret `
                    -TenantDomain $TenantDomain
            }
            if ($AccessToken) {

                # Build Parameters
                $Parameters = @{
                    AccessToken       = $AccessToken
                    Uri               = $Uri
                    CleanUpProperties = $CleanUpProperties
                    Activity          = $Activity
                }
                if ($ExcludePreviewFeatures) {
                    $Parameters.Add("ExcludePreviewFeatures", $true)
                }

                # If there are groups to update, foreach group with a group id
                if ($AzureADGroups) {
                    
                    # Update groups
                    Invoke-WTGraphPatch `
                        @Parameters `
                        -InputObject $AzureADGroups
                }
                else {
                    $ErrorMessage = "There are no Azure AD groups to be updated"
                    Write-Error $ErrorMessage
                }
            }
            else {
                $ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
                Write-Error $ErrorMessage
                throw $ErrorMessage
            }
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    End {
        try {
            
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
}

Create an Azure AD group

The next function is New-WTAzureADGroup, which you can access from my GitHub.

This creates new Azure AD groups, which will typically be imported from config files, or defined by another function, such as a Conditional Access inclusion/exclusion group in the pipeline.

Examples below:

Expand code block
# Clone repo that contains the Graph API and ToolKit functions
git clone --branch main --single-branch https://github.com/wesley-trust/GraphAPI.git
git clone --branch main --single-branch https://github.com/wesley-trust/ToolKit.git

# Dot source function into memory
. .\GraphAPI\Public\AzureAD\Groups\New-WTAzureADGroup.ps1

# Define Variables
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$DisplayName = "SVC-CA; Service Accounts"

# Create input object
$AzureADGroup = [PSCustomObject]@{
  displayName     = $DisplayName
  mailEnabled     = $false
  securityEnabled = $true
}

# Pipe the Azure AD group to the function, specify an access token previously obtained
$AzureADGroup | New-WTAzureADGroup -AccessToken $AccessToken

# Or specify each parameter individually, including an access token previously obtained
New-WTAzureADGroup -AccessToken $AccessToken -AzureADGroup $AzureADGroup

What does this do?

  • This sets specific variables, including the activity and the Graph Uri
    • As well as properties to remove from the input that would cause errors as they are readonly (or not recognised)
    • The properties are cleaned up within the private patch function
  • An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
  • A mailNickName is a required unique parameter, if one is not specified in the Azure AD group, one is generated
    • This uses the New-WTRandomString function to generate a string, which is concatenated with the Service variable
  • The private function is then called

The complete function as at this date, is below:

Expand code block (always grab the latest version from GitHub)
function New-WTAzureADGroup {
    [cmdletbinding()]
    param (
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client ID for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientID,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client secret for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientSecret,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The initial domain (onmicrosoft.com) of the tenant"
        )]
        [string]$TenantDomain,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The access token, obtained from executing Get-WTGraphAccessToken"
        )]
        [string]$AccessToken,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude features in preview, a production API version will be used instead"
        )]
        [switch]$ExcludePreviewFeatures,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            ValueFromPipeLine = $true,
            HelpMessage = "Specify the Azure AD Groups to create"
        )]
        [Alias('AzureADGroup')]
        [PSCustomObject]$AzureADGroups
    )
    Begin {
        try {
            # Function definitions
            $Functions = @(
                "GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
                "GraphAPI\Private\Invoke-WTGraphPost.ps1",
                "Toolkit\Public\New-WTRandomString.ps1"
            )

            # Function dot source
            foreach ($Function in $Functions) {
                . $Function
            }

            # Variables
            $Activity = "Creating Azure AD groups"
            $Uri = "groups"
            $CleanUpProperties = (
                "id",
                "createdDateTime",
                "modifiedDateTime",
                "SideIndicator",
                "securityIdentifier",
                "createdByAppId",
                "renewedDateTime",
                "SVC",
                "REF",
                "ENV"
            )
            $Service = "AD"

        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    Process {
        try {

            # If there is no access token, obtain one
            if (!$AccessToken) {
                $AccessToken = Get-WTGraphAccessToken `
                    -ClientID $ClientID `
                    -ClientSecret $ClientSecret `
                    -TenantDomain $TenantDomain
            }
            if ($AccessToken) {
                
                # Build Parameters
                $Parameters = @{
                    AccessToken       = $AccessToken
                    Uri               = $Uri
                    CleanUpProperties = $CleanUpProperties
                    Activity          = $Activity
                }
                if ($ExcludePreviewFeatures) {
                    $Parameters.Add("ExcludePreviewFeatures", $true)
                }
                
                # If there are groups to deploy, for each
                if ($AzureADGroups) {

                    # Foreach group, check whether the required mailNickname exists, if not, generate this, append and return group
                    $AzureADGroups = foreach ($Group in $AzureADGroups){
                        if (!$Group.mailNickname){
                            $mailNickname = $null
                            $mailNickname = $Service + "-" + (New-WTRandomString -CharacterLength 24 -Alphanumeric)
                            $Group | Add-Member -MemberType NoteProperty -Name "mailNickname" -Value $mailNickname
                        }
                        
                        # Return group
                        $Group
                    }
                    
                    # Create groups
                    Invoke-WTGraphPost `
                        @Parameters `
                        -InputObject $AzureADGroups
                }
                else {
                    $ErrorMessage = "There are no groups to be created"
                    Write-Error $ErrorMessage
                }
            }
            else {
                $ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
                Write-Error $ErrorMessage
                throw $ErrorMessage
            }
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    End {
        try {
            
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
}

Remove an Azure AD group

The next function is Remove-WTAzureADGroup, which you can access from my GitHub.

This removes Azure AD groups by Id, and can be used in the pipeline for example, to remove Conditional Access inclusion/exclusion groups when a Conditional Access policy is deleted.

Examples below:

Expand code block
# Clone repo that contains the Graph API functions
git clone --branch main --single-branch https://github.com/wesley-trust/GraphAPI.git

# Dot source function into memory
. .\GraphAPI\Public\AzureAD\Groups\Remove-WTAzureADGroup.ps1

# Define Variables
$IDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"

# Pipe specific IDs to get to the function, including an access token previously obtained
$IDs | Remove-WTAzureADGroup -AccessToken $AccessToken

# Or specify each parameter individually, including an access token previously obtained
Remove-WTAzureADGroup -AccessToken $AccessToken -IDs $IDs

What does this do?

  • This sets specific variables, including the activity and the Graph Uri
  • An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
  • The private function is then called

The complete function as at this date, is below:

Expand code block (always grab the latest version from GitHub)
function Remove-WTAzureADGroup {
    [cmdletbinding()]
    param (
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client ID for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientID,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client secret for the Azure AD service principal with Azure AD group Graph permissions"
        )]
        [string]$ClientSecret,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The initial domain (onmicrosoft.com) of the tenant"
        )]
        [string]$TenantDomain,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The access token, obtained from executing Get-WTGraphAccessToken"
        )]
        [string]$AccessToken,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude features in preview, a production API version will be used instead"
        )]
        [switch]$ExcludePreviewFeatures,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            ValueFromPipeLine = $true,
            HelpMessage = "The Azure AD Groups to remove, this must contain valid id(s)"
        )]
        [Alias("id", "GroupID", "GroupIDs")]
        [string[]]$IDs
    )
    Begin {
        try {
            # Function definitions
            $Functions = @(
                "GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
                "GraphAPI\Private\Invoke-WTGraphDelete.ps1"
            )

            # Function dot source
            foreach ($Function in $Functions) {
                . $Function
            }

            # Variables
            $Activity = "Removing Azure AD groups"
            $Uri = "groups"

        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    Process {
        try {

            # If there is no access token, obtain one
            if (!$AccessToken) {
                $AccessToken = Get-WTGraphAccessToken `
                    -ClientID $ClientID `
                    -ClientSecret $ClientSecret `
                    -TenantDomain $TenantDomain
            }
            if ($AccessToken) {
                
                # Build Parameters
                $Parameters = @{
                    AccessToken       = $AccessToken
                    Uri               = $Uri
                    Activity          = $Activity
                }
                if ($ExcludePreviewFeatures) {
                    $Parameters.Add("ExcludePreviewFeatures", $true)
                }
                
                # If there are policies to be removed,  remove them
                if ($IDs) {
                    Invoke-WTGraphDelete `
                        @Parameters `
                        -IDs $IDs
                }
                else {
                    $ErrorMessage = "There are no Ids specified which are required to remove groups"
                    Write-Error $ErrorMessage
                }
            }
            else {
                $ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
                Write-Error $ErrorMessage
                throw $ErrorMessage
            }
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    End {
        try {
            
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
}

Export an Azure AD group

The last function is Export-WTAzureADGroup, which you can access from my GitHub.

This exports the group config information from Azure AD to a JSON file. Within the pipeline this allows newly created or updated groups to have the updated config committed back to the repo for version control.

Examples below:

Expand code block
# Clone repo that contains the Graph API and ToolKit functions
git clone --branch main --single-branch https://github.com/wesley-trust/GraphAPI.git
git clone --branch main --single-branch https://github.com/wesley-trust/ToolKit.git

# Dot source function into memory
. .\GraphAPI\Public\AzureAD\Groups\Export-WTAzureADGroup.ps1

# Define Variables
$IDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$Path = "GraphAPIConfig\AzureAD\Groups"

# Export all groups from Azure AD to the path specified, including an access token previously obtained
Export-WTAzureADGroup -AccessToken $AccessToken -Path $Path

# Or pipe specific IDs to the function to export to the path specified, including an access token previously obtained
$IDs | Export-WTAzureADGroup -AccessToken $AccessToken -Path $Path

# Or specify each parameter individually, including an access token previously obtained
Export-WTAzureADGroup -AccessToken $AccessToken -Path $Path -IDs $IDs

What does this do?

  • This sets specific variables, including optional properties to cleanup from the config prior to export
    • As well as the RegEx for unsupported characters for Windows, which are replaced with underscores
    • Plus the tags to be evaluated for the groups, as well as the property ‘displayName’ to tag
  • An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
  • if no groups are specified, all groups are obtained unless there are specific ids provided
  • A subdirectory is created if it does not exist to store the policies, if a tag exists, and they should be included
    • A JSON file is then created per group as required

The complete function as at this date, is below:

Expand code block (always grab the latest version from GitHub)
function Export-WTAzureADGroup {
    [cmdletbinding()]
    param (
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client ID for the Azure AD service principal with AzureAD Graph permissions"
        )]
        [string]$ClientID,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Client secret for the Azure AD service principal with AzureAD Graph permissions"
        )]
        [string]$ClientSecret,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The initial domain (onmicrosoft.com) of the tenant"
        )]
        [string]$TenantDomain,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The access token, obtained from executing Get-WTGraphAccessToken"
        )]
        [string]$AccessToken,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The path where the JSON file(s) will be created"
        )]
        [string]$Path,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The file path where the JSON file will be created"
        )]
        [string]$FilePath,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude features in preview, a production API version will be used instead"
        )]
        [switch]$ExcludePreviewFeatures,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude the cleanup operations of the groups to be exported"
        )]
        [switch]$ExcludeExportCleanup,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "Specify whether to exclude tag processing of groups"
        )]
        [switch]$ExcludeTagEvaluation,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The AzureAD groups to get, this must contain valid id(s), when not specified, all groups are returned"
        )]
        [Alias("Group", "AzureADGroup")]
        [pscustomobject]$AzureADGroups,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The AzureAD groups to get, this must contain valid id(s), when not specified, all groups are returned"
        )]
        [Alias("id", "GroupID", "GroupIDs")]
        [string[]]$IDs,
        [parameter(
            Mandatory = $false,
            ValueFromPipeLineByPropertyName = $true,
            HelpMessage = "The tag to use as the subdirectory to organise the export, default is 'SVC'"
        )]
        [Alias("Tag")]
        [string]$DirectoryTag = "SVC"
    )
    Begin {
        try {
            # Function definitions
            $Functions = @(
                "GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
                "GraphAPI\Public\AzureAD\Groups\Get-WTAzureADGroup.ps1",
                "Toolkit\Public\Invoke-WTPropertyTagging.ps1"
            )

            # Function dot source
            foreach ($Function in $Functions) {
                . $Function
            }
            
            # Variables
            $CleanUpProperties = (
                "id",
                "createdDateTime",
                "modifiedDateTime"
            )
            $UnsupportedCharactersRegEx = '[\\\/:*?"<>|]'
            $Tags = @("SVC", "REF", "ENV")
            $PropertyToTag = "DisplayName"
            $Delimiter = "-"
            $Counter = 1
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    Process {
        try {

            # If group object is provided, tag these
            if ($AzureADGroups) {

                # Evaluate the tags on the policies to be created, if not set to exclude
                if (!$ExcludeTagEvaluation) {
                    $AzureADGroups = Invoke-WTPropertyTagging -Tags $Tags -QueryResponse $AzureADGroups -PropertyToTag $PropertyToTag
                }
            }
            
            # If there are no groups to export, get groups based on specified parameters
            if (!$AzureADGroups) {
                
                # If there is no access token, obtain one
                if (!$AccessToken) {
                    $AccessToken = Get-WTGraphAccessToken `
                        -ClientID $ClientID `
                        -ClientSecret $ClientSecret `
                        -TenantDomain $TenantDomain
                }

                if ($AccessToken) {

                    # Build Parameters
                    $Parameters = @{
                        AccessToken = $AccessToken
                    }
                    if ($ExcludeTagEvaluation) {
                        $Parameters.Add("ExcludeTagEvaluation", $true)
                    }
                    if ($ExcludePreviewFeatures) {
                        $Parameters.Add("ExcludePreviewFeatures", $true)
                    }
                    if ($IDs) {
                        $Parameters.Add("GroupIDs", $IDs)
                    }
                    
                    # Get all AzureAD groups
                    $AzureADGroups = Get-WTAzureADGroup @Parameters

                    if (!$AzureADGroups) {
                        $ErrorMessage = "Microsoft Graph did not return a valid response"
                        Write-Error $ErrorMessage
                        throw $ErrorMessage
                    }
                }
                else {
                    $ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
                    Write-Error $ErrorMessage
                    throw $ErrorMessage
                }
            }

            # If there are groups
            if ($AzureADGroups) {

                # Sort and filter (if applicable) groups
                $AzureADGroups = $AzureADGroups | Sort-Object displayName
                if (!$ExcludeExportCleanup) {
                    $AzureADGroups | Foreach-object {
                            
                        # Cleanup properties for export
                        foreach ($Property in $CleanUpProperties) {
                            $_.PSObject.Properties.Remove("$Property")
                        }
                    }
                }

                # Export to JSON
                Write-Host "Exporting AzureAD Groups (Count: $($AzureADGroups.count))"

                # If a file path is specified, output all groups in one JSON formatted file
                if ($FilePath) {
                    $AzureADGroups | ConvertTo-Json -Depth 10 `
                    | Out-File -Force -FilePath $FilePath
                }
                else {
                    foreach ($Group in $AzureADGroups) {

                        # Remove characters not supported in Windows file names
                        $GroupDisplayName = $Group.displayname -replace $UnsupportedCharactersRegEx, "_"
                        
                        # Concatenate directory, if not set to exclude, else, append tag
                        if (!$ExcludeTagEvaluation) {
                            if ($Group.$DirectoryTag) {
                                $Directory = "$DirectoryTag$Delimiter$($Group.$DirectoryTag)"
                            }
                            else {
                                $Directory = "\"
                            }
                        }
                        else {
                            $Directory = "\"
                        }
                            
                        # If directory path does not exist for export, create it
                        $TestPath = Test-Path $Path\$Directory -PathType Container
                        if (!$TestPath) {
                            New-Item -Path $Path\$Directory -ItemType Directory | Out-Null
                        }

                        # Output current status
                        Write-Host "Processing Group $Counter with file name: $GroupDisplayName.json"
                            
                        # Output individual Group JSON file
                        $Group | ConvertTo-Json -Depth 10 `
                        | Out-File -Force -FilePath "$Path\$Directory\$GroupDisplayName.json"

                        # Increment counter
                        $Counter++
                    }
                }
            }
            else {
                $WarningMessage = "There are no AzureAD groups to export"
                Write-Warning $WarningMessage
            }
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
    End {
        try {
            
        }
        catch {
            Write-Error -Message $_.Exception
            throw $_.exception
        }
    }
}