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 ๐
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
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.