Manage Azure AD group relationships via the Graph API & PowerShell
Managing Azure AD group members is a dependency for the Conditional Access policies, as for (almost) every policy I’ll be adding members to groups. Such as the nested groups that will be added to the inclusion/exclusion groups created within the pipeline.
This creates a complete solution that can be deployed in an Azure Pipeline.
Managing Azure AD group relationships
- Get Azure AD group relationships
- Create Azure AD group relationships
- Remove Azure AD group relationships
Get Azure AD group relationships
The first function is Get-WTAzureADGroupRelationship, which you can access from my GitHub.
This gets the Azure AD group owners, memberOf or members, for the specific group IDs specified.
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\Relationship\Get-WTAzureADGroupRelationship.ps1
# Define Variables
$ClientID = "sdg23497-sd82-983s-sdf23-dsf234kafs24"
$ClientSecret = "khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"
$TenantDomain = "wesleytrustsandbox.onmicrosoft.com"
$GroupIDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$Relationship = "members"
# Create hashtable
$Parameters = @{
ClientID = $ClientID
ClientSecret = $ClientSecret
TenantDomain = $TenantDomain
GroupIDs = $GroupIDs
Relationship = $Relationship
}
# Get the members for the specific group, splat the parameters (including the service principal to obtain an access token)
Get-WTAzureADGroupRelationship @Parameters
# Or pipe specific group IDs to get the members, including an access token previously obtained
$GroupIDs | Get-WTAzureADGroupRelationship -AccessToken $AccessToken -Relationship $Relationship
# Or specify each parameter individually, including an access token previously obtained
Get-WTAzureADGroupRelationship -AccessToken $AccessToken -GroupIDs $GroupIDs -Relationship $Relationship
What does this do?
- This sets specific variables, including the activity, the tags to be evaluated in the relationships, and the Graph Uri
- A group relationship could consist of owners, memberOf or members which is validated
- 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, 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-WTAzureADGroupRelationship {
[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 = $true,
ValueFromPipeLineByPropertyName = $true,
ValueFromPipeLine = $true,
HelpMessage = "The Azure AD group to get the members of, this must contain valid id(s)"
)]
[Alias("id", "GroupID", "GroupIDs")]
[string[]]$IDs,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The group relationship to return, such as group members, owners or groups this group is a member of"
)]
[ValidateSet("members", "owners", "memberOf", "assignLicense")]
[string]$Relationship
)
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 group $Relationship"
$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)
}
# Get Azure AD group relationship
$QueryResponse = foreach ($Id in $IDs) {
Invoke-WTGraphGet @Parameters -Uri "$Uri/$Id/$Relationship"
}
# Return response if one is returned
if ($QueryResponse) {
$QueryResponse
}
else {
$WarningMessage = "No group $Relationship exist in Azure AD for any of the group IDs 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
}
}
}
Create Azure AD group relationships
The next function is New-WTAzureADGroupRelationship, which you can access from my GitHub.
This creates new Azure AD group relationships, which can be owners, members as well as assigned licences, this is used within the pipeline to add members to the Conditional Access inclusion/exclusion groups created in the pipeline.
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\Relationship\New-WTAzureADGroupRelationship.ps1
# Define Variables
$ClientID = "sdg23497-sd82-983s-sdf23-dsf234kafs24"
$ClientSecret = "khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"
$TenantDomain = "wesleytrustsandbox.onmicrosoft.com"
$GroupID = "gb5d3497-78jb-983s-hb5s6-gbv334kafs24"
$RelationshipIDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$Relationship = "members"
# Create hashtable
$Parameters = @{
ClientID = $ClientID
ClientSecret = $ClientSecret
TenantDomain = $TenantDomain
GroupID = $GroupID
RelationshipIDs = $RelationshipIDs
Relationship = $Relationship
}
# Add new relationships to the specified group, splat the parameters (including the service principal to obtain an access token)
New-WTAzureADGroupRelationship @Parameters
# Or pipe specific relationship IDs to create the association with the group, including an access token previously obtained
$RelationshipIDs | New-WTAzureADGroupRelationship -AccessToken $AccessToken -GroupID $GroupID -Relationship $Relationship
# Or specify each parameter individually, including an access token previously obtained
New-WTAzureADGroupRelationship -AccessToken $AccessToken -GroupID $GroupID -RelationshipIDs $RelationshipIDs -Relationship $Relationship
What does this do?
- This sets specific variables, including the activity and the Graph Uri
- A group relationship could consist of owners, members as well as assigned licences, which is validated
- An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
- A group ID is required to add a relationship, this forms part of the Uri, the request must in a specific format
- To add a relationship, an object must be created in a specific format, this is done for each relationship ID
- The private function is then called with the collection of object relationships to be added to the group
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function New-WTAzureADGroupRelationship {
[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 = $true,
ValueFromPipeLineByPropertyName = $true,
ValueFromPipeLine = $true,
HelpMessage = "The Azure AD group to add the members or owners to, this must contain valid id(s)"
)]
[Alias("GroupID")]
[string]$ID,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The group relationship to add, such as group members or owners"
)]
[ValidateSet("members", "owners", "assignLicense")]
[string]$Relationship,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The relationship ids of the objects to add to the group"
)]
[Alias('RelationshipID', 'GroupRelationshipID', 'GroupRelationshipIDs')]
[string[]]$RelationshipIDs
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
"GraphAPI\Private\Invoke-WTGraphPost.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Activity = "Adding Azure AD group $Relationship"
$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
Activity = $Activity
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
if ($Relationship -eq "assignLicense") {
$Parameters.Add("Uri", "$Uri/$Id/$Relationship")
}
else {
$Parameters.Add("Uri", "$Uri/$Id/$Relationship/`$ref")
}
# If there are IDs, for each, create an appropriate object with the IDs
if ($RelationshipIDs) {
if ($Relationship -eq "assignLicense") {
$Licences = foreach ($RelationshipId in $RelationshipIDs) {
[PSCustomObject]@{
"disabledPlans" = @()
"skuId" = $RelationshipId
}
}
$RelationshipObject = [PSCustomObject]@{
addLicenses = @(
$Licences
)
removeLicenses = @()
}
}
else {
$RelationshipObject = foreach ($RelationshipId in $RelationshipIDs) {
[PSCustomObject]@{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$RelationshipId"
}
}
}
# Add group relationship
Invoke-WTGraphPost `
@Parameters `
-InputObject $RelationshipObject
}
else {
$ErrorMessage = "There are no group $Relationship to be added"
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 Azure AD group relationships
The last function is Remove-WTAzureADGroupRelationship, which you can access from my GitHub.
This removes Azure AD group relationships, which can be owners, members as well as assigned licences.
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\Relationship\Remove-WTAzureADGroupRelationship.ps1
# Define Variables
$ClientID = "sdg23497-sd82-983s-sdf23-dsf234kafs24"
$ClientSecret = "khsdfhbdfg723498345_sdfkjbdf~-SDFFG1"
$TenantDomain = "wesleytrustsandbox.onmicrosoft.com"
$GroupID = "gb5d3497-78jb-983s-hb5s6-gbv334kafs24"
$RelationshipIDs = @("gkg23497-43gf-983s-5fg36-dsf234kafs24","hsw23497-hg5d-t59b-fd35k-dsf234kafs24")
$AccessToken = "HWYLAqz6PipzzdtPwRnSN0Socozs2lZ7nsFky90UlDGTmaZY1foVojTUqFgm1vw0iBslogoP"
$Relationship = "members"
# Create hashtable
$Parameters = @{
ClientID = $ClientID
ClientSecret = $ClientSecret
TenantDomain = $TenantDomain
GroupID = $GroupID
RelationshipIDs = $RelationshipIDs
Relationship = $Relationship
}
# Remove relationships from the specified group, splat the parameters (including the service principal to obtain an access token)
Remove-WTAzureADGroupRelationship @Parameters
# Or pipe specific relationship IDs to remove the association with the group, including an access token previously obtained
$RelationshipIDs | Remove-WTAzureADGroupRelationship -AccessToken $AccessToken -GroupID $GroupID -Relationship $Relationship
# Or specify each parameter individually, including an access token previously obtained
Remove-WTAzureADGroupRelationship -AccessToken $AccessToken -GroupID $GroupID -RelationshipIDs $RelationshipIDs -Relationship $Relationship
What does this do?
- This sets specific variables, including the activity and the Graph Uri
- A group relationship could consist of owners, members as well as assigned licences, which is validated
- An access token is obtained, if one is not provided, this allows the same token to be shared within the pipeline
- A group ID is required to remove a relationship, the relationship IDs are also specified as part of the Uri
- The private function is then called for each ID to be removed from the group
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Remove-WTAzureADGroupRelationship {
[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 = $true,
ValueFromPipeLineByPropertyName = $true,
ValueFromPipeLine = $true,
HelpMessage = "The Azure AD group to remove the members or owners from, this must contain valid id(s)"
)]
[Alias("GroupID")]
[string]$ID,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The group relationship to remove, such as group members or owners"
)]
[ValidateSet("members", "owners", "assignLicense")]
[string]$Relationship,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The relationship ids of the objects to remove from the group"
)]
[Alias('RelationshipID', 'GroupRelationshipID', 'GroupRelationshipIDs')]
[string[]]$RelationshipIDs
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Public\Authentication\Get-WTGraphAccessToken.ps1",
"GraphAPI\Private\Invoke-WTGraphPost.ps1",
"GraphAPI\Private\Invoke-WTGraphDelete.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Activity = "Removing Azure AD group $Relationship"
$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
Activity = $Activity
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
# If there are IDs, for each, where appropriate create an object and remove the group relationship with the appropriate function
if ($RelationshipIDs) {
if ($Relationship -eq "assignLicense") {
$Licences = foreach ($RelationshipId in $RelationshipIDs) {
[PSCustomObject]@{
"skuId" = $RelationshipId
}
}
$RelationshipObject = [PSCustomObject]@{
addLicences = @()
removeLicenses = @(
$Licences
)
}
# Remove group relationship
Invoke-WTGraphPost `
@Parameters `
-InputObject $RelationshipObject
}
else {
foreach ($RelationshipId in $RelationshipIDs) {
# Remove group relationship
Invoke-WTGraphDelete `
@Parameters `
-Uri "$Uri/$Id/$Relationship/$RelationshipId/`$ref"
}
}
}
else {
$ErrorMessage = "There are no group $Relationship to be removed"
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
}
}
}