Table of Contents
A cmdlet around the endpoint, or a cmdlet per method?
Disclaimer: every REST API works different, so this script should be an example of how you can build a function for each REST API.
Nowadays there are more and more Rest APIs that we can use instead of Cmdlets (which use the Rest API in the background).
Microsoft also has a few:
The advantage of a Rest API is that you have much more control over how and what you want to send and what is included by default.
The disadvantage is often that system administrators build their own PowerShell module around a Rest API and then fall back on creating Cmdlets with pre-made parameters and default parameters that are included by default.
The cmdlets seem dynamic, but they are not, because well, they only work on one endpoint.
In this blog post I will show you how you can make your cmdlets even more dynamic with less fuss, and how a junior engineer can use the cmdlets.
Let’s decide how we will build the functions!
I’m going to assume that we’re going to work with PowerShell functions.
You can decide for yourself whether you want to make a PowerShell module of this.
Within the Rest API you have different methods and I’m going to talk about the 4 most common methods here (In the APIs that I work with).
GET
: TheGET
method requests a representation of the specified resource. Requests usingGET
should only retrieve data and should not contain a request content.- .
POST
ThePOST
method submits an entity to the specified resource, often causing a change in state or side effects on the server. PATCH
: ThePATCH
method applies partial modifications to a resource.DELETE
: TheDELETE
method deletes the specified resource.
Powered by Mozilla Firefox
We now have 2 options. Either we make 1 mega function that can handle all methods (including more than shown above), or we make 4 separate functions out of this.
I’m not going to lie, option 1 sounds very tempting, because for me it would work fine, but if we have to take into account that other people have to understand it too, then I would say let’s make four separate functions.
Who knows, maybe one day there will be a blog post to make 1 rule them all function.
Let’s create 4 new files for each method we’ve chosen
I want to keep Microsoft’s approved verbs for PowerShell intact, so we don’t create a Post or Patch, we create cmdlets with the correct verbs.
After the verb-
a shortcode is often used so that you know that these functions belong together.
I like to use BW, but do what you want.
My new functions are all called:
Get-BWGet
New-BWPost
Set-BWPatch
Remove-BWDelete
Finally, we need to choose which Rest API we are going to build this for.
The Microsoft Graph API is in the basics easy to understand with most documentation from Microsoft.
We will start with Get-BWGet
(Should be Get-BW
)
I will focus mostly on the Get so that I only have to tell the story once.
The Get method is the easiest because we don’t need to provide a body.
So we’ll start with the easiest one.
I will also explain most in the Get since the rest can be so
Let’s build the function as a PowerShell advanced function first:
Function Get-BWGet
{
[CmdletBinding()]
param ()
begin{}
process{}
}
Now that we have the basics, we need to think about the minimum parameters we need.
Normally I would include an authentication or authorization in this, but because it is not a module but separate functions, I now assume that someone logs in via Connect-AzAccount
and reuses the accessToken from Get-AzAccessToken
.
So, we need AccessToken as a parameter, we need to know which Uri, we need to know what the content-type will be, but since most times it’s ‘application/json
‘ we’ll keep the default on that.
Function Get-BWGet {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[string]$AccessToken,
[Parameter()]
[string]$ContentType = 'application/json'
)
begin {}
process {}
}
First we build a header and because Microsoft Graph expects a bearer token, we build the $headers
in the begin{}
block.
We can provide the content type via the header but can also be done via parameter -ContentType
.
Function Get-BWGet {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[string]$AccessToken,
[Parameter()]
[string]$ContentType = 'application/json'
)
begin {
$Headers = @{
'Authorization' = "Bearer $AccessToken"
'Content-Type' = $ContentType
}
}
process {
$InvokeRestMethodSplat = @{
Uri = $Uri
Headers = $Headers
Method = 'Get'
}
Invoke-RestMethod @InvokeRestMethodSplat
}
}
Now we need a uri & accessToken for the Graph API to get a return.
See the code block below what I’ve done.
Connect-AzAccount
$AccessToken = (Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com').Token
$GetBWGetSplat = @{
Uri = 'https://graph.microsoft.com/v1.0/me'
AccessToken = $AccessToken
}
Get-BWGet @GetBWGetSplat
displayName : Bas Wijdenes (Admin)
givenName : Bas
...
This is the basics, but you can go much further.
For example,
- you can check in the
begin{}
block if the access token has expired and automatically request a new one IN the code, - or you can check if a top query has been processed and add it if it is not present,
- you could check if you do not get data back on the v1.0 or if the beta does return data.
you can edit the get method endlessly to get the best result, and you can retrieve all the endpoints that the Graph API has with that one function.
The only thing your junior has to do is logon with Connect-AzAccount
, grab the access token & know which endpoint it should call which he can look up on Microsoft Graph REST API.
Do you need more inspiration? You can check the Get-Mga
from the Optimized.Mga module.
Get user – Microsoft Graph v1.0 | Microsoft Learn
Remove-BWDelete
(because it doesn’t contain a body)
We first grab the delete method because a delete is not allowed to contain a body and we can copy the base from the get method.
Function Remove-BWDelete {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[string]$AccessToken,
[Parameter()]
[string]$ContentType = 'application/json'
)
begin {
$Headers = @{
'Authorization' = "Bearer $AccessToken"
'Content-Type' = $ContentType
}
}
process {
$InvokeRestMethodSplat = @{
Uri = $Uri
Headers = $Headers
Method = 'Delete'
}
Invoke-RestMethod @InvokeRestMethodSplat
}
}
And what has been changed? The function name and the method type has been changed to delete.
Shouldn’t we add a switch to the above function for, get and delete? That’s up to you, but if one of your users makes a mistake and does a delete instead of a get is a painful mistake.
And customization can be a pain when you need to make modifications for get and delete seperately.
I rather keep them separated.
$RemoveBWDeleteSplat = @{
Uri = 'https://graph.microsoft.com/v1.0/users/3d1c5207-5334-4916-933a-a17bc20407b7'
AccessToken = $AccessToken
}
Remove-BWDelete @RemoveBWDeleteSplat
And delete does not give a response (statuscode 204), so you should trust the process that the user is deleted! I double checked though:
Resource '3d1c5207-5334-4916-933a-a17bc20407b7' does not exist or one of its queried reference-property objects are not present.
And again, you can do all sorts of customization to make the delete method work as well as possible with the Rest API you are building it for.
For some inspiration I share the Remove-Mga
from the Optimized.Mga module.
Delete a user – Microsoft Graph v1.0
New-BWPost
We make another copy, but now we need to change more than just the function name and method. Post contains a body.
Function New-BWPost {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[string]$AccessToken,
[Parameter(Mandatory)]
[object]$Body,
[Parameter()]
[string]$ContentType = 'application/json'
)
begin {
$Headers = @{
'Authorization' = "Bearer $AccessToken"
'Content-Type' = $ContentType
}
try {
$Body = $Body | ConvertFrom-Json
}
catch {
$Body = $Body
}
$Body = $Body | ConvertTo-Json
}
process {
$InvokeRestMethodSplat = @{
Uri = $Uri
Headers = $Headers
Method = 'Post'
Body = $Body
}
Invoke-RestMethod @InvokeRestMethodSplat
}
}
I added a $Body
Parameter. The body is an [object]
for now, which means it will accept whatever it is.
Microsoft Graph expects the request body to be Json, because I don’t want users to think about this, I create a try {} catch {}
block to make sure the body is always Json.
Now, let’s create a user!
You can build the body as Json or as a PowerShell object, see the code below as an example.
$body = @{
accountEnabled = $true
displayName = "Adele Vance"
mailNickname = "AdeleV"
userPrincipalName = "[email protected]"
passwordProfile = @{
forceChangePasswordNextSignIn = $true
password = "xWwvJ]6NMw+bWH-d"
}
}
$NewBWPostSplat = @{
Uri = 'https://graph.microsoft.com/v1.0/users'
AccessToken = $AccessToken
Body = $body
}
New-BWPost @NewBWPostSplat
displayName : Adele Vance
userPrincipalName : [email protected]
Besides creating a user, we can use the same method to create a group and basically anything where a post is used in the Microsoft Graph API.
So no separate cmdlets for users, groups and more.
For inspiration: New-Mga
.
Create User – Microsoft Graph v1.0
Last but not least, Set-BWPatch
Patch also uses a body, so we can copy the method from Post again.
Function Set-BWPatch {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$Uri,
[Parameter(Mandatory)]
[string]$AccessToken,
[Parameter(Mandatory)]
[object]$Body,
[Parameter()]
[string]$ContentType = 'application/json'
)
begin {
$Headers = @{
'Authorization' = "Bearer $AccessToken"
'Content-Type' = $ContentType
}
try {
$Body = $Body | ConvertFrom-Json
}
catch {
$Body = $Body
}
$Body = $Body | ConvertTo-Json
}
process {
$InvokeRestMethodSplat = @{
Uri = $Uri
Headers = $Headers
Method = 'Patch'
Body = $Body
}
Invoke-RestMethod @InvokeRestMethodSplat
}
}
In theory I think you can merge these, but since we are now making separate functions, I will also make a separate function for this.
I updated the function name and method again.
I don’t want you to log in with the above account so I want to disable the account.
$body = @{
accountEnabled = $false
}
$SetBWPatchSplat = @{
Uri = 'https://graph.microsoft.com/v1.0/users/a63c5771-a5f1-4d70-b213-b47bec5b7164'
AccessToken = $AccessToken
Body = $body
}
Set-BWPatch @SetBWPatchSplat
And a patch does not give a return response (204 – no content), but the account is disabled.
For inspiration again: Set-Mga
.
Update user – Microsoft Graph v1.0
Conclusion
The foundation has now been laid.
You can use this with almost all (Microsoft) Rest APIs and convert it to other Rest APIs.
Now you can start with customizations for the Microsoft Graph API, such as automatically adding queries, paging, batching, throttling and more and you can use Optimized.Mga as an example.
There may be a follow-up to this blog post where I will go deeper into the customizations.