- Monitoring Microsoft 365 with SCOM and the NiCE Active 365 Management Pack - Tue, Feb 7 2023
- SCOM.Addons.MailIn: Monitor anything that can send email with SCOM - Mon, May 25 2020
- Display a user’s logged-on computer in Active Directory Users and Computers (ADUC) - Mon, Jan 21 2019
Polaris
Representational State Transfer (REST) provides a standardized interface commonly used for machine-to-machine communication. With Invoke-RestMethod, PowerShell provides a cmdlet to consume RESTful services conveniently.
Polaris is "a cross-platform, minimalist web framework for PowerShell." By using this framework, you can easily build your own RESTful webservice with only PowerShell. Polaris is free, open source, and also written in PowerShell.
If you'd like to contribute, visit GitHub. The mind behind Polaris is Tyler Leonhardt. Together with Micah Rairdon, he actively maintains the project on GitHub.
Terms
A short and comprehensive explanation of terms follows. These are useful when working with RESTful webservices.
JSON
JavaScript Object Notation (JSON) provides a way to conserve structured data like objects, properties, and lists and their relation to each other. Serialization is the procedure to convert virtual objects to be conservable. Deserialization is the other way around. Polaris uses JSON to send and retrieve information, for example:
myObj = { "name":"John", "age":30, "cars": { "car1":"Ford", "car2":"BMW", "car3":"Fiat" } }
PowerShell offers easy handling with ConvertFrom-Json and ConvertTo-Json.
CRUD
CRUD stands for Create, Read, Update, and Delete. A (REST) API should provide this common set of functions. We use C to create a user, R to read users, U to update attributes, and D to delete a user.
Routing
Usually URLs serve to access websites that use files on a webserver. Routing allows using URLs that do not map to a physical file but instead to a function called (e.g., via CRUD) from a webservice.
Mapping
Mapping is the process of allocating an HTTP method to a webservice function. A common practice is the following:
REST method | Webservice function |
POST | C: Create |
GET | R: Read |
PUT | U: Update |
DELETE | D: Delete |
Use case: Manage AD users via REST
Many peripheral systems use AD data. These systems include configuration management databases (CMDBs), IT service management (ITSM) tools, and enterprise resource planning (ERP) systems. Providing a REST interface can be a good solution if they're not integrated into AD, do not provide LDAP, or run on a Linux webserver that can't be integrated.
You should first make sure you have the latest PowerShell version installed on a Windows Server that will host Polaris. Polaris requires at least PowerShell 5.1. You can then download Polaris from the PowerShell Gallery and install it on a Windows Server.
PowerShell for the logic of the Webservice
Next, we will go through the PowerShell code executed when calling a REST function. One script file named Webserivce.AD.Usr.Mgmt.ps1 holds all the code.
The route and working directory is /adsvc and exists as a directory on the webserver within the Polaris Module path.
Middleware
<# Executes the middleware every time a request comes in; ensures that the directory is set for each request and that the request body is already converted from JSON. #> Import-Module -Name Polaris $polarisPath = 'C:\Program Files\WindowsPowerShell\Modules\Polaris' $middleWare = @" `$PolarisPath = '$polarisPath\ADSvc' if (-not (Test-Path `$PolarisPath)) { `$null = New-Item -Path `$PolarisPath -ItemType Directory } if (`$Request.BodyString -ne `$null) { `$Request.Body = `$Request.BodyString | ConvertFrom-Json } `$Request | Add-Member -Name PolarisPath -Value `$PolarisPath -MemberType Noteproperty "@ New-PolarisRouteMiddleware -Name JsonBodyParser -ScriptBlock ([scriptblock]::Create($middleWare)) -Force
DELETE – Method
<# Deletes the user account specified after the Identity parameter. Remove -WhatIf if tested fully. For example: Invoke-RestMethod -Method Delete -Uri http://localhost:8081/adsvc?Identity='textilie456' Where 'textitlie456' is the samAccountName of a user account. #> New-PolarisDeleteRoute -path "/adsvc" -ScriptBlock { $adUserIdentity = $request.Query['Identity'] if ($adUserIdentity) { $adUserInfos = Remove-ADUser $adUserIdentity -WhatIf $response.Send(($adUserInfos | ConvertTo-Json)) } else { $response.SetStatusCode(501) $respone.Send("UserID not specified.") return } }
READ method
<# READ method <# Queries AD either for a single user if using Identity or for many users if using Filter in the URL. For example: Invoke-RestMethod -Method Get -Uri http://localhost:8081/adsvc?Identity='textilie456' Where 'textitlie456' is the samAccountName of a user account. #> New-PolarisGetRoute -Path "/adsvc" -ScriptBlock { $adUserFilter = '' $adUserIdentity = '' #Retrieves values for Filter or Identity from the URL $adUserFilter = $request.Query['Filter'] $adUserIdentity = $request.Query['Identity'] if ($adUserFilter) { $getADUserParams = @{Filter = $adUserFilter} } if ($adUserIdentity) { $getADUserParams = @{Identity = $adUserIdentity} } #Checks if either Filter or Identity as filled. If not, it will be return an error. if ($adUserFilter -or $adUserIdentity) { $adUserInfos = Get-ADUser @getADUserParams $response.Send(($adUserInfos | ConvertTo-Json)) } else { $adUserInfos = 'Invalid Get-ADUser call.' $response.Send(($adUserInfos | ConvertTo-Json)) $respone.SetStatusCode(501) return } } -Force
UPDATE method
<# Modifies user account properties. For removing, adding, or replacing, use hashtable syntax. For clearing, a single attribute is enough. For example, this clears the description for the samAccountName ENUsr005: $jsonUpdate = @{ Identity = 'ENUsr005' Clear = 'description' } | ConvertTo-Json Invoke-RestMethod -Method Put -Uri http://localhost:8081/adsvc -Body $jsonUpdate ContentType application/json #> New-PolarisPutRoute -Path "/adsvc" -ScriptBlock { $setUserIdentity = '' $setUserAdd = '' $setUserReplace = '' $setUserParams = @{} $setUserIdentiy = $request.Body.Identity $setUserAdd = $request.Body.Add $setUserReplace = $request.Body.Replace $setUserRemove = $request.Body.Remove $setUserClear = $request.Body.Clear Function ConvertFrom-SerializedParams { <# ConvertFrom-SerializedParams is a helper function that replaces JSON-incompatible characters and converts the string from the URL to a hashtable to use from the AD cmdlets. #> param ( [object]$usrInput, [ref]$fnOutPut ) $regPatValue = "(?<=\=)[\w\.\-,_\|'@= ]*" $regPatName = '[\w\.\-_\|\s]*(?=\=)' $propHash = New-Object -TypeName System.Collections.Hashtable if ($usrInput -match ';') { $usrInput.Split(';') | ForEach-Object { $propName = [Regex]::Match($_,$regPatName) | Select-Object ExpandProperty Value $propValue = [Regex]::Match($_,$regPatValue) | Select-Object ExpandProperty Value $propValue = $propValue.Replace("|",".") $propHash.Add($propName, $propValue) } } else { $propName = [Regex]::Match($usrInput,$regPatName) | Select-Object ExpandProperty Value $propValue = [Regex]::Match($usrInput,$regPatValue) | Select-Object ExpandProperty Value $propValue = $propValue.Replace("|",".") $propHash.Add($propName, $propValue) } $fnOutPut.Value = $propHash } #end Function ConvertFrom-SerializedParams if ($setUserIdentiy -and ($setUserAdd -or $setUserReplace -or $setUserRemove -or $setUserClear)) { $setUserParams.Add('Identity', $setUserIdentiy) if ($setUserAdd) { $setUserAddpropHash = '' ConvertFrom-SerializedParams -usrInput $setUserAdd -fnOutput ([ref]$setUserAddpropHash) $setUserParams.Add('Add', $setUserAddpropHash) } if ($setUserReplace) { $setUserReplpropHash = '' ConvertFrom-SerializedParams -usrInput $setUserReplace -fnOutput ([ref]$setUserReplpropHash) $setUserParams.Add('Replace', $setUserReplpropHash) } if ($setUserRemove) { $setUserRemovepropHash = '' ConvertFrom-SerializedParams -usrInput $setUserRemove -fnOutput ([ref]$setUserRemovepropHash) $setUserParams.Add('Remove', $setUserRemovepropHash) } if ($setUserClear) { $setUserClear = $setUserClear.Replace("|",".") $setUserParams.Add('Clear', $setUserClear) } $adUserInfos = Set-ADUser @setUserParams $response.Send(($adUserInfos | ConvertTo-Json)) } else { $respone.SetStatusCode(501) $respone.Send("UserID or option not specified.") return } #end if ($setUserIdentiy -and ($setUserAdd -or $setUserReplace -or $setUserRemove -or $setUserClear)) } -Force
CREATE method
<# Creates a user object with all parameters specified in the request. For example: Invoke-RestMethod -Method Post -Uri http://localhost:8081/adsvc -Body $jsonCreate ContentType application/json At the time of this writing, Polaris does not seem to support SSL directly. It is possible with a change in the source: https://github.com/PowerShell/Polaris/issues/107 To establish a minimal layer of security, changing the encoding obfuscates the password. Details are at https://ss64.com/ps/out-string.html #> New-PolarisPostRoute -Path "/adsvc" -ScriptBlock { $adUserProperties = '' $adUserProperties = $request.Body.UserProperties Function ConvertFrom-SerializedParams { <# ConvertFrom-SerializedParams is a helper function that replaces JSON-incompatible characters and converts the string from the URL to a hashtable to use from the AD cmdlets. #> param ( [object]$usrInput, [ref]$fnOutPut ) $regPatValue = "(?<=\=)[\w\.\-,_\|'@= ]*" $regPatName = '[\w\.\-_\|\s]*(?=\=)' $propHash = New-Object -TypeName System.Collections.Hashtable if ($usrInput -match ';') { $usrInput.Split(';') | ForEach-Object { $propName = [Regex]::Match($_,$regPatName) | Select-Object ExpandProperty Value $propValue = [Regex]::Match($_,$regPatValue) | Select-Object ExpandProperty Value $propValue = $propValue.Replace("|",".") $propHash.Add($propName, $propValue) } } else { $propName = [Regex]::Match($usrInput,$regPatName) | Select-Object ExpandProperty Value $propValue = [Regex]::Match($usrInput,$regPatValue) | Select-Object ExpandProperty Value $propValue = $propValue.Replace("|",".") $propHash.Add($propName, $propValue) } $fnOutPut.Value = $propHash } #end Function ConvertFrom-SerializedParams if ($adUserProperties) { $newUserpropHash = '' ConvertFrom-SerializedParams -usrInput $adUserProperties -fnOutput ([ref]$newUserpropHash) $hiddenPassword = $newUserpropHash.Item('AccountPassword') $clearPassword = '' $clearPassword = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($hiddenPassword)) $validPassWord = ConvertTo-SecureString -string $clearPassword -AsPlainText Force $newUserpropHash.Item('AccountPassword') = $validPassWord $adNewUserInfos = New-ADUser @newUserpropHash $response.Send(($adNewUserInfos | ConvertTo-Json)) } else { $adNewUserInfos = 'Invalid New-ADUser call.' $response.Send(($adNewUserInfos | ConvertTo-Json)) $respone.SetStatusCode(501) return } }
Start Polaris
#STARTING POLARIS on port 8018. Don't forget to open the port in your firewall. Start-Polaris -Port 8081
Consuming the webservice with PowerShell
To use the webservice, we can use PowerShell. You can find a description of the PowerShell function in its comment section.
Subscribe to 4sysops newsletter!
Helper function
Function ConvertTo-SerializedParams { <# ConvertTo-SerializedParams is a helper function. It replaces the dot, an invalid JSON character, with a pipe character. It converts the hashtable to a string we can convert to JSON later on. #> param ( [hashtable]$hashTableIn, [ref]$serializedOut ) $outPutString = '' $hashTableIn.GetEnumerator() | ForEach-Object { $value = $_.Value.ToString().Replace(".","|") $outPutString += $([char]34) + $_.Key + '=' + $value + $([char]34) + ';' } $outPutString = $outPutString.Substring(0,$outPutString.Length - 1) $serializedOut.Value = $outPutString } #end Function ConvertTo-SerializedParams
DELETING
#DELETES a user account named textilie456 Invoke-RestMethod -Method Delete -Uri http://localhost:8081/adsvc?Identity='textilie456'
READING
<# READ the user with samAccountName USREN1001. The replace method is required since this returns ObjectGuid two times in different cases. Replaces one occurrence with ObjectGuid_ (ending with an underscore). The conversion to JSON fails because it is not case sensitive. #> $oneUser = Invoke-RestMethod -Method Get -Uri http://localhost:8081/adsvc?Identity=USREN1001 $oneUser -replace 'ObjectGuid','ObjectGuid_' | ConvertFrom-Json <# READ all users matching a Name attribute of mueller; the UrlEncode method ensures the filter matches a valid URL such as replacing spaces with %20 #> $userFilter = 'Name -like "*mueller*"' $validUrl = 'http://localhost:8081/adsvc?filter=' + [System.Web.HttpUtility]::UrlEncode($userFilter) $someUsers = Invoke-RestMethod -Method Get -Uri $validUrl $someUsers
UPDATING
#Adds a description 'admin' and the URL 'www.4sysops.com' to a user with samAccountName USRGB005 $additionsHash = @{description="admin";url="www.4sysops.com"} $additions = '' ConvertTo-SerializedParams -hashTableIn $additionsHash -serializedOut ([ref]$additions) $jsonUpdate = @{ Identity = 'USRGB005' Add = $additions } | ConvertTo-Json Invoke-RestMethod -Method Put -Uri http://localhost:8081/adsvc -Body $jsonUpdate ContentType application/json #Clears the description for the user with samAccountName USRGB005 $jsonUpdate = @{ Identity = 'USRGB005' Clear = 'description' } | ConvertTo-Json Invoke-RestMethod -Method Put -Uri http://localhost:8081/adsvc -Body $jsonUpdate ContentType application/json
CREATING
<# Creates a user with the samAccountName testilie456 and other required parameters. Obfuscates the password to have a minimum set of security. #> $passWordClear = '123Security,123' $passWordHidden = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($passWordClear), 'InsertLineBreaks') $newUserParams = @{ AccountPassword = $passWordHidden UserPrincipalName = 'textilie456@nwtraders.msft' Surname = 'LastNameAB' GivenName = 'First NameAB' DisplayName = 'LastName, First NameAB' Path = 'OU=Users,OU=Location,DC=nwtraders,DC=msft' SamAccountName = 'textilie456' Name = 'LastName, First NameAB' } $newUserInfo = '' ConvertTo-SerializedParams -hashTableIn $newUserParams -serializedOut ([ref]$newUserInfo) $jsonCreate = @{ UserProperties = $newUserInfo } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri http://localhost:8081/adsvc -Body $jsonCreate ContentType application/json
Closing
Brandon Olin demonstrated another use case of Polaris in his 4sysops post on How to run a PowerShell script as a Windows service. To learn more about latest PowerShell technologies such as Polaris, Pipelines, Testing and much more, I recommend the book Learn PowerShell Core 6.0: Automate and control administrative tasks using DevOps principles by David das Neves and Jan-Hendrik Peters.
One question which I can’t find an easy answer to: how does Polaris (or this application) manage authentication and authorisation?
Hi Andrew,
this ‘application’ doesn’t actively manage authentication and authorization. In my setup the users authentication was passed through automatically and AD just gave the access my user account was delegated to.
There are active issues on Github where this topic is discussed. Feel free to add your thoughts to existing discussions here: https://github.com/powershell/polaris/issues
Doing so will show more interest and hopefully a focus in developing it further.
There is a matured alternative to Polaris called ‘PowerShell Universal Dashboards’. It offers similar functionality plus the capabilities you are missing.
Update 2018-11-09:
Add basic authentication for Polaris
Hope it helps
Ruben
A short update on this point. A few hours ago the development team confirmed that windows authentication provides and SSL are now natively supported.
https://github.com/PowerShell/Polaris/pull/150
https://twitter.com/JeremyMcgee73/status/1069697012864442368
Documentation is in progress.
Updates!
Jeremy McGee’s, a new member in the Polaris developer community blogged about how to add certificates to Polaris:
SSL support for Polaris
Among others, Windows Authentication is now possible, too! Find the example code on:
Windows Authentication Support for Polaris
This looks interesting, I am interested in looking at the Webserivce.AD.Usr.Mgmt.ps1 will it be possible to get the file to try it out.