Birth right permissions using Azure AD Entitlement Management access packages

Currently, automatically assigning users to access packages is not a feature available. It is on the Microsoft product group’s agenda, but not on public roadmap yet.

So I built a little PowerShell workaround, which works fine.

The way I have done it is that a script is run on a scheduled basis, finding all request policies named “Automatic group assignment”, auto-requesting the access package for the user.

So, start by creating the following access package request policy:

Add new policy
Name “Automatic group assignment”
Choose “For users in your directory” and specify a group (Script currently does not support multiple groups, but can easily be added as a feature)
Enable the policy with “No” approval
We can disable all life cycle features, as it is the group membership that causes this access package to be assigned

Ok, so now you have an access package with a policy named “Automatic group assignment”, that allows an Azure AD group’s members to request the access package without any approval. What we need to do now is to have a script that does this for request for the user.

In order to run the script, we need an app registration and a user.

Create a user, with no Azure AD role (user is enough, no need for Global Admin or anything), and the ability to sign in without MFA. Assign the user the the role of “Access package manager” on each Entitlement Management catalog where you need this feature:

Create a new app registration, and grant the following permissions:

On the authentication page, enable “Treat application as public client”. This is to enable ROPC:

Now you are ready for the script. The first 10 lines is configuration you need to modify, while the rest can remain as is:

Github url

# Client ID and secret for the app registration
$clientid = "281d6e42-99c9-44a7-a24e-c80971210fff"
$clientsecret = read-host -Prompt "Client secret"

# Username and password for a user with access package manager role
$username = "Entitlement.Management.Automation@tenantname.onmicrosoft.com"
$password = read-host -Prompt "Password"

# Any domain registered in the tenant, such as tenantname.onmicrosoft.com
$tenant = "tenantname.onmicrosoft.com"

#
# Should not need to be touched anything below this line:
#
$VerbosePreference = "Continue"

$body = "client_id=$clientid&username=$username&password=$password&grant_type=password&scope=user.read.all%20group.read.all%20EntitlementManagement.ReadWrite.All"
$token = Invoke-RestMethod "https://login.microsoftonline.com/$tenant/oauth2/v2.0/token" -Body $body -Method Post

$restParams = @{Headers = @{Authorization = "$($token.token_type) $($token.access_token)"}}
 
# Endpoints
$graphBase = "https://graph.microsoft.com/beta"
$endpoints = @{
    accessPackageCatalogs = "{0}/identityGovernance/entitlementManagement/accessPackageCatalogs" -f $graphBase
    accessPackages = "{0}/identityGovernance/entitlementManagement/accessPackages" -f $graphBase
    accessPackageAssignments = "{0}/identityGovernance/entitlementManagement/accessPackageAssignments" -f $graphBase
    users = "{0}/users" -f $graphBase
    groups = "{0}/groups" -f $graphBase
    accessPackageAssignmentPolicies = "{0}/identityGovernance/entitlementManagement/accessPackageAssignmentPolicies" -f $graphBase
    me = "{0}/me" -f $graphBase
    directoryObjects = "{0}/directoryObjects" -f $graphBase
    accessPackageAssignmentRequests = "{0}/identityGovernance/entitlementManagement/accessPackageAssignmentRequests" -f $graphBase
}


<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
#>
function Get-GraphRequestRecursive
{
    [CmdletBinding()]
    [Alias()]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [string] $url
    )

    Write-Debug "Fetching url $url"
    $Result = Invoke-RestMethod $url @restParams -Verbose:$false
    if($Result.value) {
        $result.value
    }

    # Calls itself when there is a nextlink
    if($Result.'@odata.nextLink') {
        Get-GraphRequestRecursive $Result.'@odata.nextLink'
    }
}


#
# Get all access packages with all assignment policies
#
Write-Verbose "Getting all access packages" -Verbose 
$AccessPackages = New-Object System.Collections.ArrayList

Get-GraphRequestRecursive "$($endpoints.accessPackages)?`$expand=accessPackageAssignmentPolicies" | Foreach {
    $AccessPackages.Add($_) | Out-Null
}



#
# Get all access package assignment policies
#

$AccessPackageAssignmentPolicies = $AccessPackages | 
    ForEach-Object {$_.accessPackageAssignmentPolicies} |
    Where-Object displayName -eq "Automatic group assignment" |
    ForEach-Object {'{0}/{1}?$expand=accessPackage' -f $endpoints.accessPackageAssignmentPolicies, $_.id} |
    ForEach-Object {Invoke-RestMethod $_ @restParams}


#
# Get all assigned users to the access packages relevant for the access package assignment policies
#
Write-Verbose "Getting all users assigned to access packages having an automatic group assignment access package policy..."
$Assignments = $AccessPackageAssignmentPolicies |
    Where-Object displayName -eq "Automatic group assignment" | # Only query policies called "Automatic group assignment"
    Sort-Object -Unique -Property accessPackageId | # Can only request assignments on a per access package level, so let's only query each access package once
    ForEach-Object {
        # Build query url and run recursive get
        $url = '{0}?$filter=accessPackage/id eq ''{1}''&$expand=target' -f $endpoints.accessPackageAssignments, $_.accessPackageId 
        Get-GraphRequestRecursive -url $url
    }

$AssignmentsMap = @{}
$Assignments | 
    Where-Object assignmentState -in "Delivered" |
    ForEach-Object {
        if(!$AssignmentsMap.ContainsKey($_.assignmentPolicyId)) {
            $AssignmentsMap[$_.assignmentPolicyId] = New-Object System.Collections.ArrayList
        }
        $AssignmentsMap[$_.assignmentPolicyId].Add($_) | Out-Null
    }


#
# Processing all assignments by getting group members and combining with current assignments
#
Write-Verbose "Processing all assignments by getting group members and combining with current assignments..."
$ProcessedAccessPackageAssignmentPolicies = $AccessPackageAssignmentPolicies | 
    Where-Object displayName -eq "Automatic group assignment" |
    ForEach-Object {
        Write-verbose " - Processing access package assignment policy $($_.id) for access package $($_.accessPackage.displayName)"
        $obj = @{
            CurrentAssignments = $AssignmentsMap[$_.id]
            Id = $_.id
            GroupObjectId = $_.requestorSettings.allowedRequestors.id
            AccessPackageId = $_.accessPackageId
            AccessPackageName = $_.accessPackage.displayName
            GroupMembers = @()
            ShouldNotBeMembers = @()
            MissingMembers = @()
        }


        # Get all group members
        Write-verbose "   - Getting members for group $($obj.GroupObjectId)..."
        $groupUrl = "{0}/{1}/members" -f $endpoints.groups, $obj.GroupObjectId
        $obj.GroupMembers = Get-GraphRequestRecursive $groupUrl
        
        # Calculate which members should be removed or added
        $obj.MissingMembers = $obj.GroupMembers | Where-Object {$_.id -notin $obj.CurrentAssignments.target.objectid}
        $obj.ShouldNotBeMembers = $obj.CurrentAssignments | Where-Object {$_.target.objectid -notin $obj.GroupMembers.id}

        [PSCustomObject] $obj
    }

#
# Processing access package assignment policies with users that should not be members
# 
Write-Verbose "Processing access package assignment policies with users that should not be members"

$ProcessedAccessPackageAssignmentPolicies | 
    Where-Object ShouldNotBeMembers | 
    ForEach-Object {
        Write-Verbose " - Processing access package policy $($_.id) for access package $($_.AccessPackageName)"
        $AccessPackageName = $_.AccessPackageName
        $_.ShouldNotBeMembers | ForEach-Object {
            Write-Verbose "   - Removing user $($_.id) from access package policy $($_.AssignmentPolicyId) for access package $($AccessPackageName)"

            $body = @{
                requestType = "AdminRemove"
                accessPackageAssignment = @{
                    id = $_.id
                    assignmentPolicyId = $_.assignmentPolicyId
                    accessPackageId = $_.AccessPackageId
                }
            } | ConvertTo-Json

            Invoke-RestMethod -Method Post -Uri $endpoints.accessPackageAssignmentRequests -Body $body -ContentType "application/json" @restParams -Verbose:$false | Out-Null
        }
    }


#
# Processing access package assignment policies with missing members
# 
Write-Verbose "Processing access package assignment policies with missing members"

$ProcessedAccessPackageAssignmentPolicies | 
    Where-Object MissingMembers | 
    ForEach-Object {
        Write-Verbose " - Processing access package policy $($_.id)"
        $AccessPackagePolicyId = $_.Id 
        $AccessPackageId = $_.AccessPackageId
        $AccessPackageName = $_.AccessPackageName
        $_.MissingMembers | ForEach-Object {
            Write-Verbose "   - Adding user $($_.id) to access package policy $($AccessPackagePolicyId) for access package $($AccessPackageName)"

            $body = @{
                requestType = "AdminAdd"
                accessPackageAssignment = @{
                    targetId = $_.id
                    assignmentPolicyId = $AccessPackagePolicyId
                    accessPackageId = $AccessPackageId
                }
            } | ConvertTo-Json

            Invoke-RestMethod -Method Post -Uri $endpoints.accessPackageAssignmentRequests -Body $body -ContentType "application/json" @restParams -Verbose:$false | Out-Null
        }
    }

My suggestion is that you run this script on your Azure AD Connect server, if you have one. If you are among the very few cloud only organizations, it should be possible to run the script as an Azure Function with minor modifications.

For running as a scheduled task, you should modify the first lines as follows, running the ConvertFrom-SecureString stuff once as the user you are running the scheduled task as, pasting them on the line below:

# Client ID and secret for the app registration
$clientid = "281d6e42-99c9-44a7-a24e-c80971210ccc"
# RUN ONCE as the user you are running this script as: read-host -Prompt "Client secret" -AsSecureString  | convertfrom-securestring | set-clipboard
$clientsecret = ConvertTo-SecureString '01000000d08c9ddf0115d1118c7a00c04fc297eb0100000017cf620bf110d741a7bd87dcf631179700000000020000000000106600000001000020000000dab44a28aacc8746f735bdba64ddc34c3043476512e579ae4e991e94d766ed96000000000e80000000020000200000007630a3d537526cf2c09bbffd95c009a5db890deab4ea18ac8b54ad50f555379d100000004bbb5857f2e526dd6f4e2e023ae5cc3140000000ac4915b2811f2fbbcedbf8648b73d8c915c153d45fd0e0913f1a695eac833bdffb709388fee11cd90742ccd6dfd9cf014b58026be7d708deaae5ce7a9f9bf488'

# Username and password for a user with access package manager role
$username = "Entitlement.Management.Automation@olavthon.onmicrosoft.com"
# RUN ONCE as the user you are running this script as: read-host -Prompt "Password" -AsSecureString  | convertfrom-securestring | set-clipboard
$password = ConvertTo-SecureString "01000000d08c9ddf0115d1118c7a00c04fc297eb0100000017cf620bf110d741a7bd87dcf63117970000000002000000000010660000000100002000000079588afe9bedef7f0ab7bce07c583325dffd7b03a2cf822297c4e4d5d26429ae000000000e8000000002000020000000950670c33dba8d6c1f57a20ddb7f5cf5f1a2ecbbe0228a942454facdcb12c71d100000005c13653550cf6780b1951efba7cd5bc8400000009fbf7e197a279cb8021d93cce48cfee68cf7efb2d67989f2d69d64884332b2241a701f5aac5e0e67d714e2721685d490481a578f872e2c56a257d8c33f97b2ed"

# Any domain registered in the tenant, such as x.onmicrosoft.com
$tenant = "tenantname.onmicrosoft.com"

Good luck 🙂

One thought on “Birth right permissions using Azure AD Entitlement Management access packages

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s