ConfigMgr AdminService and WMI Methods – A match made in the cloud (1910)


This post applies to Microsoft Endpoint Configuration Manager 1910 Current Branch


At Microsoft Ignite 2019 Brad Anderson demoed a few things in his sessions, later covered more in depth in other sessions, that highlighted some new features to allow you to leverage the cloud console to manage ConfigMgr-only devices. As I watched these sessions, I realized that they were using the ConfigMgr Administration Service to do integrate into the cloud.


Watch these sessions for more background on what’s coming BRK008 - Modern management: How/why you do it now BRK2082 - What’s new in Microsoft Endpoint Manager, including Microsoft Intune and Configuration Manager (Part 1 of 2) BRK3220 - What’s new in Microsoft Endpoint Manager, including Microsoft Intune and Configuration Manager (Part 2 of 2)


Paul Mayfield showed how to sync ConfigMgr client policies on ConfigMgr-only, non co-managed, non Intune registered device (BRK2082 11:50 timestamp)

A Square Dozen Image

Brad Anderson shows a real-time application install on a ConfigMgr-only, non co-managed, non Intune registered device (BRK008 11:00 - timestamp)

A Square Dozen Image

Both of these demos use the ConfigMgr AdminService on the backend. I know I’ve been trying to sell the AdminService as a replacement for WMI, but it’s so much more. The AdminService will leverage your Cloud Management Gateway as a web proxy and allow you to take actions on Any Device from Anywhere (within reason). With Co-Managed and Intune-only devices, you already had much of this functionality; now with the Microsoft Endpoint Admin Center, you can leverage a centralized cloud console to manage devices, regardless of how they are managed.

In this post, I’d like to highlight some of the new features that have been enabled in in the AdminService in Microsoft Endpoint Configuration Manager 1910 Current Branch. *If you were at MMSJazz, you may have heard me talk about this in the 1911 Technical Preview.

WMI Methods!

Up until now, the AdminService has been largely read-only - you could only read WMI classes, but none of the WMI methods were available until now. In 1910, the WMI methods are all here! You can see them by listing the WMI route metadata and looking for ActionImport in the XML output. https://<SERVERNAME>/AdminService/wmi/$metadata

A Square Dozen Image

If you visit the reference for the WMI class in question, you will be able to find the methods and method syntax. From there, you should be able to work out how to execute any WMI method.

SMS_ClientOperation

The Inside the ConfigMgr console, the right-click menu has several options which leverage the BGB (Big Green Button) or Fast Channel to communicate with the client. These include:

  • Run Script
  • Install Application
  • Client Notification
    • Download Computer Policy
    • Download User Policy
    • Collect Discovery Data
    • Collect Software Inventory
    • Collect Hardware Inventory
    • Evaluate Application Deployments
    • Evaluate Software Update Deployments
    • Switch To Next Software Update Point
    • Evaluate Device Health Attestation
    • Check Conditional Access Compliance
    • Wake Up
    • Restart
  • Client Diagnostics
    • Enable Verbose Logging
    • Disable Verbose Logging
  • Endpoint Protection
    • Full Scan
    • Quick Scan
    • Download Definition

A Square Dozen Image

If you watch SMSProv.log when you click these, you will see that they each call methods on the SMS_ClientOperation WMI class on the SMS Provider server.

A Square Dozen Image

Each device with the ConfigMgr client installed has a WMI class called ROOT\ccm\ClientSDK. Generally, whenever you want to do things on the client, you would need to remotely connect to the device, attach to the ClientSDK class and trigger a WMI method on the client. Essentially, everything is happening client-side. This is no good when you want to take actions from something like a web API or web console. When you trigger these fast channel methods, all of the logic is in the WMI method on the server side, not the client side. I’m sure I’m doing a terrible job of describing what actually happens here… If we look up the class in the WMI reference https://docs.microsoft.com/configmgr/develop/reference/protect/sms_clientoperation-server-wmi-class we can see that there are several methods available to take actions on clients from the server. Here’s the PowerShell to trigger a reboot using the SMS_ClientOperation class using Type 17:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ServerName = ""
$SiteCode = ""
$NameSpace = "root\SMS\Site_{0}" -f $SiteCode
$ClassName = "SMS_ClientOperation"
$MethodName = "InitiateClientOperation"
[string]$TargetCollectionID = "SMS00001"
[uint32]$Type = 17 #Reboot
[uint32]$RandomizationWindow = 1
[uint32[]]$TargetResourceIDs = 16777325

#Using CIM
$Args = @{
    TargetCollectionID = $TargetCollectionID
    Type = $Type
    RandomizationWindow = $RandomizationWindow
    TargetResourceIDs = $TargetResourceIDs
}
Invoke-CimMethod -Namespace $NameSpace -ClassName $ClassName -MethodName $MethodName -Arguments $Args | Select-Object ReturnValue

Now that we’ve got a handle on BGB and the SMS_ClientOperation class, let’s see how that translates over to the AdminService.

Triggering Client Operations with AdminService

The big win here is that the client Fast Channel will allow the AdminService to trigger client actions FROM ANYWHERE. Up until now, the AdminService was limited to server-side only methods and had none of the existing WMI methods. With ConfigMgr 1910 Current Branch, we will be able to start triggering client actions from any platform that can call a web API.

Using the same URL listed earlier https://<SERVERNAME>/AdminService/wmi/$metadata to list the metadata for the WMI route, if we search for SMS_ClientOperation, we will find all of the WMI Methods from the docs listed as ActionImport lines in XML.

A Square Dozen Image
From https://docs.microsoft.com/configmgr/develop/reference/protect/sms_clientoperation-server-wmi-class

Here’s a listing of the actions available for the SMS_ClientOperation WMI class in the AdminService metadata.

A Square Dozen Image

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<ActionImport Name="SMS_ClientOperation.InitiateClientOperation" Action="SCCMGraph.SMS_ClientOperation.InitiateClientOperation"/>
<ActionImport Name="SMS_ClientOperation.InitiateClientOperationEx" Action="SCCMGraph.SMS_ClientOperation.InitiateClientOperationEx"/>
<ActionImport Name="SMS_ClientOperation.RestoreQuarantinedItem" Action="SCCMGraph.SMS_ClientOperation.RestoreQuarantinedItem"/>
<ActionImport Name="SMS_ClientOperation.AllowThreat" Action="SCCMGraph.SMS_ClientOperation.AllowThreat"/>
<ActionImport Name="SMS_ClientOperation.ExcludeScanPaths" Action="SCCMGraph.SMS_ClientOperation.ExcludeScanPaths"/>
<ActionImport Name="SMS_ClientOperation.CancelClientOperation" Action="SCCMGraph.SMS_ClientOperation.CancelClientOperation"/>
<ActionImport Name="SMS_ClientOperation.DeleteClientOperation" Action="SCCMGraph.SMS_ClientOperation.DeleteClientOperation"/>
<ActionImport Name="SMS_ClientOperation.IsClientOperationAllowed" Action="SCCMGraph.SMS_ClientOperation.IsClientOperationAllowed"/>
<ActionImport Name="SMS_ClientOperation.IsClientOperationUpdateAllowed" Action="SCCMGraph.SMS_ClientOperation.IsClientOperationUpdateAllowed"/>
<EntitySet Name="SMS_ClientOperation" EntityType="SCCMGraph.SMS_ClientOperation"/>

Now that we have the class and methods, we just need to trigger the WMI method using an OData Action. For OData, we will be using a HTTP POST method and sending a json formatted BODY as parameters to the method. The URL syntax for OData Actions used by AdminService is:

https://<ServerName>/AdminService/wmi/<WMIClassName>.<WMIMethodName>

For this example it would be:
https://<ServerName>/AdminService/wmi/SMS_ClientOperation.InitiateClientOperation

Since this is a POST method, you have to upload a body. As far as I know, you can’t do this natively in a browser and need to use a separate tool. I’m using PowerShell, but you can also use Telerik Fiddler or various other tools to do the same thing.

Here’s a sample bit of PowerShell that I put together to test out the Client Notification actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Param (
    [Parameter(Mandatory=$true,HelpMessage="Enter your server name where AdminService is running (SMS Provider Role")]
    [string]$ServerName,

    [Parameter(Mandatory=$true,HelpMessage="Enter the ResourceID of the target device")]
    [uint32[]]$TargetResourceIDs,

    [Parameter(Mandatory=$false,HelpMessage="Enter a Collection ID that the target device is in")]
    [string]$TargetCollectionID = "SMS00001"
)
   
$Types = [Ordered]@{
    "DownloadComputerPolicy" = 8
    "DownloadUserPolicy" = 9
    "CollectDiscoveryData" = 10
    "CollectSoftwareInventory" = 11
    "CollectHardwareInventory" = 12
    "EvaluateApplicationDeployments" = 13
    "EvaluateSoftwareUpdateDeployments" = 14
    "SwitchToNextSoftwareUpdatePoint" = 15
    "EvaluateDeviceHealthAttestation" = 16
    "CheckConditionalAccessCompliance" = 125
    "WakeUp" = 150
    "Restart" = 17
    "EnableVerboseLogging" = 20
    "DisableVerboseLogging" = 21
}

[uint32]$RandomizationWindow = 1
[string]$MethodClass = "SMS_ClientOperation"
[string]$MethodName = "InitiateClientOperation"
[string]$ResultClass = "SMS_ClientOperationStatus"

$Types.Keys | ForEach-Object {Write-Host $Types[$_] : $_}
[uint32]$Type = Read-Host -Prompt "Which client action?"

$PostURL = "https://{0}/AdminService/wmi/{1}.{2}" -f $ServerName,$MethodClass,$MethodName
$Headers = @{
    "Content-Type" = "Application/json"
}
$Body = @{
    TargetCollectionID = $TargetCollectionID
    Type = $Type
    RandomizationWindow = $RandomizationWindow
    TargetResourceIDs = $TargetResourceIDs
} | ConvertTo-Json
    
Invoke-RestMethod -Method Post -Uri "$($PostURL)" -Body $Body -Headers $Headers -UseDefaultCredentials | Select-Object ReturnValue

#Get Results
$GetURL = "https://{0}/AdminService/wmi/{1}" -f $ServerName,$ResultClass
(Invoke-RestMethod -Method Get -Uri "$($GetURL)" -UseDefaultCredentials).Value | Format-Table

The script will prompt for your server name and the ResourceID of the device you want to target (you can actually provide multiple ResourceIDs). The last 3 lines will query SMS_ClientOperationStatus and you will see a new entry for the ResourceID(s) that you targeted. This is all done using the newly added WMI methods in the AdminService.

A Square Dozen Image

To verify that the action is actually working, you can check the AdminService.Log and BGBServer.log on the SMS Provider server or the client log that corresponds to the action that you triggered.

AdminService.log
AdminService.log

A Square Dozen Image
BgbServer.log

The best part about WMI methods being available in AdminService now is that we can call them from anywhere. No need to use PowerShell remoting to connect to the server to call the WMI method. Any authorized user can call these methods. You can even use them in Task Sequences.

As if this wasn’t enough, the next part is even better. You may have seen my previous post where I showed how to trigger CMPivot through the Cloud Management Gateway - now you can do real-time client actions over the CMG. Sync client policies, Restart, or even Install Applications in real-time.

Triggering Client Operations Over the Internet

Next I’m going to step up the game and I’m going to trigger client actions on a Co-Managed device that isn’t on the company network from a non-company device on the internet. This will simulate being able to take actions on clients through the Microsoft Endpoint Manager Admin Center.

I’ve taken the same machine that I was testing on and moved it to a non-business network with internet only. Then I run a script to use the internet-facing URL of the AdminService on a personal device and I’m able to trigger a device restart on the internet-only device. It’s not even registered in Intune.

You will need your external URL and need to generate an authentication token. I followed Sandy’s guide here https://www.scconfigmgr.com/2019/07/16/use-configmgr-administration-service-adminservice-over-internet. I took her script and rebuilt it a bit then took the sample script from above and integrated it into a new script. Here’s the result.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
Param (
    [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Native App) - Overview. Copy the Application (client) ID")]
    [string]
    $ClientID,

    [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Native App) - Overview. Copy the Directory (tenant) ID")]
    [string]
    $TenantID,

    [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Cloud Management App) - Authentication. Copy the Redirect URI. It should start with ms-appx-web://")]
    [string]
    $RedirectURI,

    [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Cloud Management App) - Expose an API. Copy the Application ID URI")]
    [string]
    $ResourceAppIdURI,

    [Parameter(Mandatory=$True, HelpMessage = "Query your SCCM DB - SELECT ExternalEndpointName, ExternalUrl FROM vProxy_Routings WHERE ExternalEndpointName = 'AdminService'")]
    [string]
    $InternetBaseURL,

    [Parameter(Mandatory=$True, HelpMessage = "The URL to your AdminService server https://<FQDN>/AdminService")]
    [string]
    $InternalBaseURL,

    [Parameter(Mandatory=$True, HelpMessage = "Use Token Auth or Current User Auth")]
    [switch]
    $UseTokenAuth = $True,

    [Parameter(Mandatory=$True)]
    [uint64[]]$TargetResourceIDs,

    [Parameter(Mandatory=$True)]
    [string]$TargetCollectionID

)

$Main = {
    [Switch]$TryTokenAuth = $false
    If(!($UseTokenAuth.IsPresent)) {
        Try {
            $Data = Initiate-ClientAction -URL $InternetBaseURL -TargetResourceIDs $TargetResourceIDs -TargetCollectionID $TargetCollectionID
        }
        Catch {
            Write-Host "An error occurred using default credentials. Trying with Token Auth."
            $TryTokenAuth = $True
        }
    }

    If($UseTokenAuth.IsPresent -or $TryTokenAuth) {
        $AuthToken = Get-AADAuthToken -ClientID $ClientID -TenantID $TenantID -ResourceAppIDURI $ResourceAppIDURI -RedirectUri $RedirectUri -PromptForNewCredentials Auto
        # Creating header for Authorization token
        If($AuthToken) {
            $AuthHeader = @{
                'Content-Type'  = 'application/json'
                'Authorization' = "Bearer " + $AuthToken.AccessToken
                'ExpiresOn'	    = $AuthToken.ExpiresOn
            }
        }
        Else {
            Write-Host "No Auth Token Found. Exiting."
            Break;
        }
        $Data = Initiate-ClientAction -URL $InternetBaseURL -TargetResourceIDs $TargetResourceIDs -TargetCollectionID $TargetCollectionID -authHeader $authHeader
    }
    
    Return $Data.value
}

Function Initiate-ClientAction {
    Param (
        [Parameter(Mandatory=$true,HelpMessage="Enter your server name where AdminService is running (SMS Provider Role")]
        [string]$URL,

        [Parameter(Mandatory=$true,HelpMessage="Enter the ResourceID of the target device")]
        [uint32[]]$TargetResourceIDs,

        [Parameter(Mandatory=$false,HelpMessage="Enter a Collection ID that the target device is in")]
        [string]$TargetCollectionID,
        
        $authHeader
    )
    
    $Types = [Ordered]@{
        "DownloadComputerPolicy" = 8
        "DownloadUserPolicy" = 9
        "CollectDiscoveryData" = 10
        "CollectSoftwareInventory" = 11
        "CollectHardwareInventory" = 12
        "EvaluateApplicationDeployments" = 13
        "EvaluateSoftwareUpdateDeployments" = 14
        "SwitchToNextSoftwareUpdatePoint" = 15
        "EvaluateDeviceHealthAttestation" = 16
        "CheckConditionalAccessCompliance" = 125
        "WakeUp" = 150
        "Restart" = 17
        "EnableVerboseLogging" = 20
        "DisableVerboseLogging" = 21
    }

    [uint32]$RandomizationWindow = 1
    [string]$MethodClass = "SMS_ClientOperation"
    [string]$MethodName = "InitiateClientOperation"
    [string]$ResultClass = "SMS_ClientOperationStatus"

    $Types.Keys | ForEach-Object {Write-Host $Types[$_] : $_}
    [uint32]$Type = Read-Host -Prompt "Which client action?"

    $PostURL = "{0}/wmi/{1}.{2}" -f $URL,$MethodClass,$MethodName
    
    $Headers = @{
        "Content-Type" = "Application/json"
    }
    $Body = @{
        TargetCollectionID = $TargetCollectionID
        Type = $Type
        RandomizationWindow = $RandomizationWindow
        TargetResourceIDs = $TargetResourceIDs
    } | ConvertTo-Json
    
    If($authHeader) {
        Invoke-RestMethod -Method Post -Uri "$($PostURL)" -Body $Body -Headers $authHeader | Select-Object ReturnValue
    }
    Else {
        Invoke-RestMethod -Method Post -Uri "$($PostURL)" -Body $Body -Headers $Headers -UseDefaultCredentials | Select-Object ReturnValue
    }


    #Get Results
    $GetURL = "{0}/wmi/{1}" -f $URL,$ResultClass
    
    If($authHeader) {
        $Result = (Invoke-RestMethod -Method Get -Uri "$($GetURL)" -Headers $authHeader).Value | Format-Table
    }
    Else {
        (Invoke-RestMethod -Method Get -Uri "$($GetURL)" -Headers $Headers -UseDefaultCredentials).Value | Format-Table
    }
    Return $Result
}

#Modified Script from Sandy's blog post
#https://www.scconfigmgr.com/2019/07/16/use-configmgr-administration-service-adminservice-over-internet/
#Follow the instructions in her blog post for building a custom App for the AdminService.
#You TECHNICALLY can use the Native Client App that gets created when you build your CMG, but it's 
#Better to create a custom App instead of hijacking the built in one.
Function Get-AADAuthToken {
    Param (
        [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Native App) - Overview. Copy the Application (client) ID")]
        [string]
        $ClientID,
    
        [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Native App) - Overview. Copy the Directory (tenant) ID")]
        [string]
        $TenantID,
    
        [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Cloud Management App) - Authentication. Copy the Redirect URI. It should start with ms-appx-web://")]
        [string]
        $RedirectURI,
    
        [Parameter(Mandatory=$True, HelpMessage = "Home > App registrations > (Your Cloud Management App) - Expose an API. Copy the Application ID URI")]
        [string]
        $ResourceAppIdURI,
    
        [Parameter(Mandatory=$True, HelpMessage = "Change the prompt behavior to force credentials each time. https://msdn.microsoft.com/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx.")]
        [ValidateSet("Auto", "Always", "Never", "RefreshSession")]
        [string]
        $PromptForNewCredentials = "Auto"
    )
    
        #Get AAD Token for AdminService
        If(Test-Path "$($PSScriptRoot)\Auth.Json") {
            $AuthToken = Get-Content -Path "$($PSScriptRoot)\Auth.Json" | ConvertFrom-Json
            If(($AuthToken -and (Get-Date) -lt $AuthToken.ExpiresOn)) {
                Return $AuthToken
            }
        }
        If(($AuthToken -and (Get-Date) -ge $AuthToken.ExpiresOn) -or (!(Test-Path "$($PSScriptRoot)\Auth.Json"))) {
            
            $Authority = "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/authorize"
    
            $AadModule = Get-Module -Name "AzureAD" -ListAvailable
            If ($AadModule -eq $null)
            {
                Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview"
                $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable
            }
            If ($AadModule -eq $null)
            {
                Write-Error "AzureAD Powershell module not installed..."
                Write-Error "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt"
                Exit
            }
    
            If ($AadModule.count -gt 1)	{
                $Latest_Version = ($AadModule | Select-Object version | Sort-Object)[-1]
                $aadModule = $AadModule | ForEach-Object { $_.version -eq $Latest_Version.version }
                
                If ($AadModule.count -gt 1)
                {
                    $aadModule = $AadModule | Select-Object -Unique
                }
                $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
                $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
            }
            Else {
                $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
                $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
            }
    
            [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
            [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null
    
            Try
            {
                $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
                
                $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList $PromptForNewCredentials
                $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParameters).Result
    
                # If the accesstoken is valid save a Json File
                If ($authResult.AccessToken) {
                    $authResult | ConvertTo-Json | Out-File -FilePath "$($PSScriptRoot)\Auth.Json" -Force
                    return $authResult
                }
                Else {
                    Write-Error "Authorization Access Token is null, please re-run authentication..."
                    break
                }
            }
            Catch
            {
                Write-Error $_.Exception.Message
                Write-Error $_.Exception.ItemName
                Break
            }
        }
    }

& $Main

If you did it right, you should have just triggered a client action on a co-managed device. I haven’t been able to test a non-co-managed device, but I expect that the functionality we saw demoed at Ignite will bring that functionality along once it is released.

Summary

The purpose of this post was to show you that you can trigger WMI methods with the ConfigMgr AdminService in the 1910 CB release of ConfigMgr. Hopefully it has inspired new ideas for how you may use this moving forward. I’ve already spoken with a few folks who are looking at making standalone admin web frontends or leveraging the Power Platform apps to interact directly with ConfigMgr using AdminService. The possibilities are truly limitless! One last note - the AdminService is still very much a work-in-progress. If you plan to mess with it, I wouldn’t start building business processes around it just yet - I think it will get better and easier to interact with moving forward. But if you want to just start getting your hands dirty, get in and give it a test drive.