Script to add Azure AD gallery apps

The Microsoft Graph endpoint for instantiating Azure AD gallery apps is quite new, and I just needed them in order to instantiate 20+ apps based on the GitHub Enterprise Organization template. Here is my code, if someone has use for it ๐Ÿ™‚

I have left out “how to get an access token”. The permissions required is Application.ReadWrite.All.

$accesstoken = "eyJ0eXAiOiJKV1QiLCJub25jZS--- snip ---Cz57c44q23How7d1fVbCJg"


function Ensure-Application
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true,
            Position=0)]
        [String] $AccessToken,

        [Parameter(Mandatory=$false,
            Position=1)]
        [String] $TemplateName = 'GitHub Enterprise Cloud - Organization',

        [Parameter(Mandatory=$false,
            Position=2)]
        [String] $Prefix = 'Github - ',

        [Parameter(Mandatory=$false,
            Position=3)]
        [String] $Suffix = ' (Enterprise Cloud)',

        [Parameter(Mandatory=$false,
            Position=4)]
        [String[]] $Owners = @(),

        [Parameter(Mandatory=$false,
            Position=5)]
        [Boolean] $AppRoleAssignmentRequired = $true,

        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true)]
        [String] $InstanceName

        
    )

    Begin
    {
        # Build rest parameters (providing authorzation header)
        $restParams = @{Headers = @{Authorization = "Bearer $accesstoken"}}

        # Get all templates (returns all without paging)
        $template = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/applicationTemplates" @restParams |
            Select-Object -ExpandProperty value | 
            Where-Object displayName -eq $TemplateName

        if(!$template.id) {
            throw "Unable to find template '$TemplateName' in the Azure AD App Gallery"
        } else {
            Write-Debug "Found template: $($template | ConvertTo-Json)"
        }
    }
    Process
    {
        # Calculate displayname
        $DisplayName = "{0}{1}{2}" -f $Prefix, $InstanceName, $Suffix
        Write-Verbose "Calculated displayname '$DisplayName'"

        # Find existing application
        $application = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=displayName eq '$DisplayName'" @restParams |
            Select-Object -ExpandProperty Value

        # Find existing service principal
        $servicePrincipal = $null
        if($application) {
            $servicePrincipal = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appid eq '$($application.appId)'" @restParams |
                Select-Object -ExpandProperty Value
        }

        if(!$servicePrincipal) {
            Write-Verbose "Instantiating application '$DisplayName'"

            # Instantiate template
            $body = @{
                displayName = $DisplayName
            } | ConvertTo-Json
            $instantiation = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/applicationTemplates/$($template.id)/instantiate" -Method Post @restParams -Body $body -ContentType "application/json"

            # Check status of instantiation
            if($instantiation.application.appId) {
                Write-Verbose "Created application '$DisplayName' with id $($instantiation.application.id) and appid $($instantiation.application.appid)"
            } else {
                throw "Something failed when instantiating app :("
            }

            # Get application
            #$application = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/applications/$($instantiation.application.id)" @restParams |
            #    Select-Object -ExpandProperty Value

            # Get service principal
            $servicePrincipal = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appid eq '$($instantiation.application.appid)'" @restParams |
                Select-Object -ExpandProperty Value
            
            if(!$servicePrincipal) {
                throw "Could not find service principal for appid $($instantiation.application.appid)"
            }
        } else {
            Write-Verbose "Application '$DisplayName' already exists, checking settings"
        }

        # Ensure appRoleAssignmentRequired is correct
        if($servicePrincipal.appRoleAssignmentRequired -ne $AppRoleAssignmentRequired) {
            Write-Verbose "Updating AppRoleAssignmentRequired from $($servicePrincipal.appRoleAssignmentRequired) to $($AppRoleAssignmentRequired)"
            $body = @{
                appRoleAssignmentRequired = $AppRoleAssignmentRequired
            } | ConvertTo-Json
            Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)" -Method Patch -Body $body -ContentType "application/json" @restParams 
        }

        # If owners defined, change to these owners
        if($Owners) {
            # Get user objects of owners group Azure AD
            $OwnerObjects = $Owners | ForEach-Object {
                $upn = $_
                try {
                    Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$($upn)" @restParams 
                } catch {
                    throw "User $upn not found"
                }
            }

            # Get current owners
            $OwnersInAzureAD = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners" @restParams | 
                Select-Object -ExpandProperty Value

            # For each owner sendt in parameter, ensure they are added as owner
            $OwnerObjects | 
                Where-Object {
                    $_.id -notin $OwnersInAzureAD.id
                } |
                ForEach-Object {
                    Write-Verbose "Adding user $($_.userPrincipalName) as owner of app"
                    $body = @{
                        "@odata.id" = "https://graph.microsoft.com/v1.0/users/{0}" -f $_.id
                    } | ConvertTo-Json
                    Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners/`$ref" @restParams -Body $body -ContentType "application/json" -Method Post
                }

            # For each owner ni Azure AD, that should not be there, ensure they are removed as owner
            $OwnersInAzureAD  | 
                Where-Object {
                    $_.id -notin $OwnerObjects.id
                } |
                ForEach-Object {
                    Write-Verbose "Removing user $($_.id) as owner of app"
                    Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners/$($_.id)/`$ref" @restParams -Method Delete
                }
        }
    }
    End
    {

    }
}

Ensure-Application -AccessToken $accesstoken -InstanceName "Test1" -Verbose -Owners @("AdeleV@M365x330461.OnMicrosoft.com")
Ensure-Application -AccessToken $accesstoken -InstanceName "Test2" -Verbose -Owners @("AdeleV@M365x330461.OnMicrosoft.com")
Ensure-Application -AccessToken $accesstoken -InstanceName "Test3" -Verbose -Owners @("AdeleV@M365x330461.OnMicrosoft.com")

This will create three applications in your tenant, all based on the same Azure AD gallery app, and add owners to the app. The script can be run several times and will only create the app with the same name once.

Good luck ๐Ÿ™‚

2 thoughts on “Script to add Azure AD gallery apps

  1. Hello would really appreciate some feedback on this.

    Script that I am running. Basically it’s your script with some minor changes.

    $TenantId = “XXXXXXXXXXXXXX”
    $ClientId = “XXXXXXXXXXXXXXX”
    $Secret = “XXXXXXXXXXXXXXXXXX”

    $uri = “https://login.microsoft.com/$TenantId/oauth2/token”

    $bdy = @{
    grant_type = “client_credentials”
    client_id = $ClientId
    client_secret = $Secret
    resource = “https://graph.microsoft.com”
    }

    $at = Invoke-RestMethod -Method Post -Uri $uri -Body $bdy -ContentType “application/x-www-form-urlencoded”

    $accesstoken = $at.access_token

    function Ensure-Application
    {
    [CmdletBinding()]
    Param
    (
    [Parameter(Mandatory=$true,
    Position=0)]
    [String] $AccessToken,

    [Parameter(Mandatory=$false,
    Position=1)]
    [String] $TemplateName = ‘GitHub Enterprise Cloud – Organization’,

    [Parameter(Mandatory=$false,
    Position=2)]
    [String] $Prefix = ‘GitHub – ‘,

    [Parameter(Mandatory=$false,
    Position=3)]
    [String] $Suffix = ”,

    [Parameter(Mandatory=$false,
    Position=4)]
    [String[]] $Owners = @(),

    [Parameter(Mandatory=$false,
    Position=5)]
    [Boolean] $AppRoleAssignmentRequired = $true,

    [Parameter(Mandatory=$true,
    ValueFromPipeline=$true)]
    [String] $InstanceName

    )

    Begin
    {
    # Build rest parameters (providing authorzation header)
    $restParams = @{Headers = @{Authorization = “Bearer $accesstoken”}}

    # Get all templates (returns all without paging)
    $template = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/applicationTemplates” @restParams |
    Select-Object -ExpandProperty value |
    Where-Object displayName -eq $TemplateName

    if(!$template.id) {
    throw “Unable to find template ‘$TemplateName’ in the Azure AD App Gallery”
    } else {
    Write-Debug “Found template: $($template | ConvertTo-Json)”
    }
    }
    Process
    {
    # Calculate displayname
    $DisplayName = “{0}{1}{2}” -f $Prefix, $InstanceName, $Suffix
    Write-Verbose “Calculated displayname ‘$DisplayName'”

    # Find existing application
    $application = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/applications?`$filter=displayName eq ‘$DisplayName'” @restParams |
    Select-Object -ExpandProperty Value

    # Find existing service principal
    $servicePrincipal = $null
    if($application) {
    $servicePrincipal = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appid eq ‘$($application.appId)'” @restParams |
    Select-Object -ExpandProperty Value
    }

    if(!$servicePrincipal) {
    Write-Verbose “Instantiating application ‘$DisplayName'”

    # Instantiate template
    $body = @{
    displayName = $DisplayName
    } | ConvertTo-Json
    $instantiation = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/applicationTemplates/$($template.id)/instantiate” -Method Post @restParams -Body $body -ContentType “application/json”

    # Check status of instantiation
    if($instantiation.application.appId) {
    Write-Verbose “Created application ‘$DisplayName’ with id $($instantiation.application.id) and appid $($instantiation.application.appid)”
    } else {
    throw “Something failed when instantiating app :(”
    }

    # Get application
    $application = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/applications/$($instantiation.application.id)” @restParams |
    Select-Object -ExpandProperty Value

    # Get service principal
    $servicePrincipal = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appid eq ‘$($instantiation.application.id)'” @restParams |
    Select-Object -ExpandProperty Value

    if(!$servicePrincipal) {
    throw “Could not find service principal for appid $($instantiation.application.appid)”
    }
    } else {
    Write-Verbose “Application ‘$DisplayName’ already exists, checking settings”
    }

    # Ensure appRoleAssignmentRequired is correct
    if($servicePrincipal.appRoleAssignmentRequired -ne $AppRoleAssignmentRequired) {
    Write-Verbose “Updating AppRoleAssignmentRequired from $($servicePrincipal.appRoleAssignmentRequired) to $($AppRoleAssignmentRequired)”
    $body = @{
    appRoleAssignmentRequired = $AppRoleAssignmentRequired
    } | ConvertTo-Json
    Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)” -Method Patch -Body $body -ContentType “application/json” @restParams
    }

    # If owners defined, change to these owners
    if($Owners) {
    # Get user objects of owners group Azure AD
    $OwnerObjects = $Owners | ForEach-Object {
    $upn = $_
    try {
    Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/users/$($upn)” @restParams
    } catch {
    throw “User $upn not found”
    }
    }

    # Get current owners
    $OwnersInAzureAD = Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners” @restParams |
    Select-Object -ExpandProperty Value

    # For each owner sendt in parameter, ensure they are added as owner
    $OwnerObjects |
    Where-Object {
    $_.id -notin $OwnersInAzureAD.id
    } |
    ForEach-Object {
    Write-Verbose “Adding user $($_.userPrincipalName) as owner of app”
    $body = @{
    “@odata.id” = “https://graph.microsoft.com/v1.0/users/{0}” -f $_.id
    } | ConvertTo-Json
    Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners/`$ref” @restParams -Body $body -ContentType “application/json” -Method Post
    }

    # For each owner ni Azure AD, that should not be there, ensure they are removed as owner
    $OwnersInAzureAD |
    Where-Object {
    $_.id -notin $OwnerObjects.id
    } |
    ForEach-Object {
    Write-Verbose “Removing user $($_.id) as owner of app”
    Invoke-RestMethod -Uri “https://graph.microsoft.com/v1.0/servicePrincipals/$($servicePrincipal.id)/owners/$($_.id)/`$ref” @restParams -Method Delete
    }
    }
    }
    End
    {

    }
    }

    Ensure-Application -AccessToken $accesstoken -InstanceName “Test2” -Verbose -Owners @(“XXX@XXX.com”,”XXX@XXX.com”)

    when i run the script, it deploys the App but fails to assign the owners.
    I am using an app registration to generate the access token and I have assigned the “Cloud Application Administrator” Role to it. Any help from you would be highly appreciated.

    Sharing the output below.

    PS C:\Users\XXX\Documents\GitHub_Enterprise> ./Test_Script.ps1
    VERBOSE: GET https://graph.microsoft.com/v1.0/applicationTemplates with 0-byte payload
    VERBOSE: received -1-byte response of content type application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
    VERBOSE: Calculated displayname ‘Github – Now (Enterprise Cloud)’
    VERBOSE: GET https://graph.microsoft.com/v1.0/applications?$filter=displayName eq ‘Github – Now (Enterprise Cloud)’ with 0-byte payload
    VERBOSE: received -1-byte response of content type application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
    VERBOSE: Instantiating application ‘Github – Now (Enterprise Cloud)’
    VERBOSE: POST https://graph.microsoft.com/v1.0/applicationTemplates/dee2cdc2-7505-4746-9758-219fec5e3da0/instantiate with -1-byte payload
    VERBOSE: received -1-byte response of content type application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
    VERBOSE: Created application ‘Github – Now (Enterprise Cloud)’ with id c23f88f5-1a2f-4635-9d75-7ff374605fdd and appid 23512932-583b-46ea-bc3a-ad24b787cac3
    VERBOSE: GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appid eq ‘23512932-583b-46ea-bc3a-ad24b787cac3’ with 0-byte payload
    VERBOSE: received -1-byte response of content type application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
    Could not find service principal for appid 23512932-583b-46ea-bc3a-ad24b787cac3
    At C:\Users\hasanmr\Documents\GitHub_Enterprise\Test_Script.ps1:113 char:17
    + … throw “Could not find service principal for appid $($inst …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (Could not find …3a-ad24b787cac3:String) [], RuntimeException
    + FullyQualifiedErrorId : Could not find service principal for appid 23512932-583b-46ea-bc3a-ad24b787cac3

  2. Try adding some lines like this after every invoke-restmethod lines with method POST:

    Start-Sleep 60

    Takes longer time to run, but this is probably happening because of some of the resiliance work Microsoft has done in Azure AD.

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 )

Facebook photo

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

Connecting to %s