PowerShell wrapped functions to call the Microsoft Graph API
Calling an API at first can seem a bit daunting for an infrastructure guy, who may be thinking that APIs are just for developers, but using an object-orientated scripting language like PowerShell, can make it far less so. You can apply all the knowledge built up over the years within your comfort zone, and interact with APIs without having to learn another programming language.
For me, to simplify things I broke down the API calls into small specific private functions, that would be in turn be called by public PowerShell functions, with the PowerShell approved verbs, matching each corresponding method of the API call:
API Method | PowerShell verb | Feature | Description |
---|---|---|---|
Get | Get | Get | To get or list objects in the resource |
Patch | Edit | Update | To update existing objects in the resource |
Post | New | Create | To create new objects in the resource |
Delete | Remove | Remove | To remove objects in the resource |
So this led to four method-specific private functions, that public functions will call, that are feature/service specific (IE Getting Azure AD groups).
These functions then call a generalised Microsoft Graph API Query function, so when public function call the API, they call based upon the method required.
Let’s break these down.
Private Functions
Private functions are not intended to be directly called, so I’ll be covering more of a top level overview of what each do, rather than providing example usage.
- Invoke a Graph query
- Invoke a Graph Get method
- Invoke a Graph Patch method
- Invoke a Graph Post method
- Invoke a Graph Delete method
Invoke a Graph query
The first function is Invoke-WTGraphQuery, which you can access from my GitHub, this is a refactored version of one Daniel created. It’s important to note, that using preview features, such as in Conditional Access, requires that the ‘beta’ API be used, this is selected by default.
What does this do?
- This allows you to specify the REST method and the Uri (uniform resource identifier), which executes against the Graph API
- This also sets up the required parameters, such as the headers and provides the Access Token in the request (provided by a public function)
- The private functions below provide the method to this query function, and the public functions provide the Uri (such as ‘groups’ for Azure AD groups)
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Invoke-WTGraphQuery {
[cmdletbinding()]
param (
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The HTTP method for the Microsoft Graph call"
)]
[ValidateSet("Get", "Patch", "Post", "Delete", "Put")]
[string]$Method,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The Uniform Resource Identifier for the Microsoft Graph API call"
)]
[string]$Uri,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The request body of the Microsoft Graph API call"
)]
[string]$Body,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
ValueFromPipeLine = $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
)
Begin {
try {
# Variables
$ResourceUrl = "https://graph.microsoft.com"
$ContentType = "application/json"
$ApiVersion = "beta" # If preview features are in use, the "beta" API must be used
# Force TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
Process {
try {
if ($AccessToken) {
# Change the API version if features in preview are to be excluded
if ($ExcludePreviewFeatures) {
$ApiVersion = "v1.0"
}
$HeaderParameters = @{
"Content-Type" = "application\json"
"Authorization" = "Bearer $AccessToken"
}
# Create an empty array to store the result
$QueryRequest = @()
$QueryResult = @()
# If the request is to get data, invoke without a body, otherwise append body
if ($Method -eq "GET") {
$QueryRequest = Invoke-RestMethod `
-Headers $HeaderParameters `
-Uri $ResourceUrl/$ApiVersion/$Uri `
-UseBasicParsing `
-Method $Method `
-ContentType $ContentType
}
else {
$QueryRequest = Invoke-RestMethod `
-Headers $HeaderParameters `
-Uri $ResourceUrl/$ApiVersion/$Uri `
-UseBasicParsing `
-Method $Method `
-ContentType $ContentType `
-Body $Body
}
# Check if a value, and if not, an ID is returned, adding either to the query result, ignoring null objects
if ($QueryRequest.value) {
$QueryResult += $QueryRequest.value
}
elseif ($QueryRequest.id) {
$QueryResult += $QueryRequest
}
# Invoke REST methods and fetch data until there are no pages left
if ("$ResourceUrl/$Uri" -notlike "*`$top*") {
while ($QueryRequest."@odata.nextLink") {
$QueryRequest = Invoke-RestMethod `
-Headers $HeaderParameters `
-Uri $QueryRequest."@odata.nextLink" `
-UseBasicParsing `
-Method $Method `
-ContentType $ContentType
$QueryResult += $QueryRequest.value
}
}
# Return query result
$QueryResult
}
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
}
}
}
Invoke a Graph Get method
You can access the Invoke-WTGraphGet function, on my GitHub.
What does this do?
- This passes the required variables of “Get” and the Access Token to the query function
- If there are specific IDs to get (rather than just performing a list) the call is altered as appropriate
- For interactive runs, a progress status bar is displayed
- The responses are tagged, if tags are provided, using the Invoke-WTPropertyTagging function
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Invoke-WTGraphGet {
[cmdletbinding()]
param (
[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 specific record ids to be returned"
)]
[Alias("id")]
[string[]]$IDs,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The uniform resource indicator"
)]
[string]$Uri,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The optional tags that could be evaluated in the response"
)]
[string[]]$Tags,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The activity being performed"
)]
[string]$Activity
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Private\Invoke-WTGraphQuery.ps1"
"Toolkit\Public\Invoke-WTPropertyTagging.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Method = "Get"
$Counter = 1
$PropertyToTag = "DisplayName"
# Output current activity
Write-Host $Activity
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
Process {
try {
if ($AccessToken) {
# Build parameters
$Parameters = @{
Method = $Method
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
# If specific policies are specified, get each, otherwise, get all policies
if ($IDs) {
$QueryResponse = foreach ($ID in $IDs) {
# Output progress
if ($IDs.count -gt 1) {
Write-Host "Processing Query $Counter of $($IDs.count) with ID: $ID"
# Create progress bar
$PercentComplete = (($counter / $IDs.count) * 100)
Write-Progress -Activity $Activity `
-PercentComplete $PercentComplete `
-CurrentOperation $ID
}
else {
Write-Host "Processing Query with ID: $ID"
}
# Increment counter
$counter++
# Get Query
$AccessToken | Invoke-WTGraphQuery `
@Parameters `
-Uri $Uri/$ID
}
}
else {
$QueryResponse = $AccessToken | Invoke-WTGraphQuery `
@Parameters `
-Uri $Uri
}
# If there is a response, and tags are defined, evaluate the response for tags or return without tagging
if ($QueryResponse) {
if ($Tags) {
Invoke-WTPropertyTagging -Tags $Tags -QueryResponse $QueryResponse -PropertyToTag $PropertyToTag
}
else {
$QueryResponse
}
}
}
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
}
}
}
Invoke a Graph Patch method
You can access the Invoke-WTGraphPatch function, on my GitHub.
What does this do?
- This passes the required variables of “Patch” and the Access Token to the query function, which corresponds to an update or ‘Edit’
- The input object could contain properties such as dates that are readonly, as well as tags, so these are removed to prevent errors
- For interactive runs, a progress status bar is displayed
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Invoke-WTGraphPatch {
[cmdletbinding()]
param (
[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 objects to be patched"
)]
[pscustomobject]$InputObject,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The uniform resource indicator"
)]
[string]$Uri,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The activity being performed"
)]
[string]$Activity,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "Properties that may exist that need to be removed prior to creation"
)]
[string[]]$CleanUpProperties
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Private\Invoke-WTGraphQuery.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Method = "Patch"
$Counter = 1
# Output current activity
Write-Host $Activity
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
Process {
try {
if ($AccessToken) {
# Build parameters
$Parameters = @{
Method = $Method
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
# If there are objects to update, foreach query with a query id
if ($InputObject) {
foreach ($Object in $InputObject) {
# Update query ID, and if exists continue
$ObjectID = $Object.id
$ObjectDisplayName = $Object.displayName
if ($ObjectID) {
# Remove properties that are not valid for when updating objects
if ($CleanUpProperties) {
foreach ($Property in $CleanUpProperties) {
$Object.PSObject.Properties.Remove("$Property")
}
}
# Convert query object to JSON
$Object = $Object | ConvertTo-Json -Depth 10
# Output progress
if ($InputObject.count -gt 1) {
Write-Host "Processing Query $Counter of $($InputObject.count) with ID: $ObjectID"
# Create progress bar
$PercentComplete = (($counter / $InputObject.count) * 100)
Write-Progress -Activity $Activity `
-PercentComplete $PercentComplete `
-CurrentOperation $ObjectDisplayName
}
else {
Write-Host "Processing Query with ID: $ObjectID"
}
# Increment counter
$counter++
# Create query, with one second intervals to prevent throttling
Start-Sleep -Seconds 1
$AccessToken | Invoke-WTGraphQuery `
@Parameters `
-Uri $Uri/$ObjectID `
-Body $Object `
| Out-Null
}
else {
$ErrorMessage = "No IDs are specified, to update an object, an ID is required"
Write-Error $ErrorMessage
}
}
}
else {
$ErrorMessage = "There are no objects to be updated, to update an object, one must be supplied"
Write-Error $ErrorMessage
throw $ErrorMessage
}
}
else {
$ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
Write-Error $ErrorMessage
throw $ErrorMessage
}
}
catch {
$ErrorMessage = "An exception has occurred, common reasons include patching properties that are not valid"
Write-Error $ErrorMessage
Write-Error -Message $_.Exception
throw $_.exception
}
}
End {
try {
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
}
Invoke a Graph Post method
You can access the Invoke-WTGraphPost function, on my GitHub.
What does this do?
- This passes the required variables of “Post” and the Access Token to the query function, which corresponds to a create or ‘New’
- The input object could contain properties such as dates that are readonly, as well as tags, so these are removed to prevent errors
- For interactive runs, a progress status bar is displayed
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Invoke-WTGraphPost {
[cmdletbinding()]
param (
[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 objects to be created"
)]
[pscustomobject]$InputObject,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The uniform resource indicator"
)]
[string]$Uri,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The activity being performed"
)]
[string]$Activity,
[parameter(
Mandatory = $false,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "Properties that may exist that need to be removed prior to creation"
)]
[string[]]$CleanUpProperties
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Private\Invoke-WTGraphQuery.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Method = "Post"
$Counter = 1
# Output current activity
Write-Host $Activity
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
Process {
try {
if ($AccessToken) {
# Build parameters
$Parameters = @{
Method = $Method
Uri = $Uri
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
# If there are policies to deploy, for each
if ($InputObject) {
foreach ($Object in $InputObject) {
# Remove properties that are not valid for when creating new objects
if ($CleanUpProperties) {
foreach ($Property in $CleanUpProperties) {
$Object.PSObject.Properties.Remove("$Property")
}
}
# Update displayname variable prior to object conversion to JSON
$ObjectDisplayName = $Object.displayName
# Convert Query object to JSON
$Object = $Object | ConvertTo-Json -Depth 10
# Output progress
if ($InputObject.count -gt 1) {
if ($ObjectDisplayName) {
Write-Host "Processing Query $Counter of $($InputObject.count) with Display Name: $ObjectDisplayName"
}
else {
Write-Host "Processing Query $Counter of $($InputObject.count)"
}
# Create progress bar
$PercentComplete = (($counter / $InputObject.count) * 100)
Write-Progress -Activity $Activity `
-PercentComplete $PercentComplete `
-CurrentOperation $ObjectDisplayName
}
else {
if ($ObjectDisplayName) {
Write-Host "Processing Query with Display Name: $ObjectDisplayName"
}
else {
Write-Host "Processing Query"
}
}
# Increment counter
$counter++
# Create record, with one second intervals to prevent throttling
Start-Sleep -Seconds 1
$AccessToken | Invoke-WTGraphQuery `
@Parameters `
-Body $Object
}
}
else {
$ErrorMessage = "There are no objects to be created, to create an object, one must be supplied"
Write-Error $ErrorMessage
throw $ErrorMessage
}
}
else {
$ErrorMessage = "No access token specified, obtain an access token object from Get-WTGraphAccessToken"
Write-Error $ErrorMessage
throw $ErrorMessage
}
}
catch {
$ErrorMessage = "An exception has occurred, common reasons include posting properties that are not valid"
Write-Error $ErrorMessage
Write-Error -Message $_.Exception
throw $_.exception
}
}
End {
try {
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
}
Invoke a Graph Delete method
You can access the Invoke-WTGraphDelete function, on my GitHub.
What does this do?
- This passes the required variables of “Delete” and the Access Token to the query function, which corresponds to a ‘Remove’
- To remove objects, the IDs of the objects must be provided
- For interactive runs, a progress status bar is displayed
The complete function as at this date, is below:
Expand code block (always grab the latest version from GitHub)
function Invoke-WTGraphDelete {
[cmdletbinding()]
param (
[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 specific record ids to be returned"
)]
[Alias("id")]
[string[]]$IDs,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The uniform resource indicator"
)]
[string]$Uri,
[parameter(
Mandatory = $true,
ValueFromPipeLineByPropertyName = $true,
HelpMessage = "The activity being performed"
)]
[string]$Activity
)
Begin {
try {
# Function definitions
$Functions = @(
"GraphAPI\Private\Invoke-WTGraphQuery.ps1"
)
# Function dot source
foreach ($Function in $Functions) {
. $Function
}
# Variables
$Method = "Delete"
$Counter = 1
# Output current activity
Write-Host $Activity
}
catch {
Write-Error -Message $_.Exception
throw $_.exception
}
}
Process {
try {
if ($AccessToken) {
# Build parameters
$Parameters = @{
Method = $Method
}
if ($ExcludePreviewFeatures) {
$Parameters.Add("ExcludePreviewFeatures", $true)
}
# If there are objects to be removed,
if ($IDs) {
foreach ($ID in $IDs) {
# Output progress
if ($IDs.count -gt 1) {
Write-Host "Processing Query $Counter of $($IDs.count) with ID: $ID"
# Create progress bar
$PercentComplete = (($counter / $IDs.count) * 100)
Write-Progress -Activity $Activity `
-PercentComplete $PercentComplete `
-CurrentOperation $ID
}
else {
Write-Host "Processing Query with ID: $ID"
}
# Increment counter
$counter++
# Remove record, one second apart to prevent throttling
Start-Sleep -Seconds 1
$AccessToken | Invoke-WTGraphQuery `
@Parameters `
-Uri $Uri/$ID `
| Out-Null
}
}
else {
$ErrorMessage = "No IDs are specified, to remove an object, an ID is required"
Write-Error $ErrorMessage
throw $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
}
}
}