The second – and updated – tool in the WaaS Toolbox – Self-Service Deferral

New week and new tool! This is the remake of an older script that you can read about here (and also where you’ll get instructions on how to get started with Azure Automation – which ill rewrite when the toolbox is a bit more complete.

So, what does this one do? As I always say when talking about Windows as a Service, the hardest and most important challenge to solve is user experience. User rarely wants to get a restart in the middle of what they are doing – or even worse, a crashed PC or application in the middle of a project. Therefore, in my opinion, its vital that we give our users more choice when it comes to how and when to install a new feature upgrade (or quality update).

One of the approaches we can use is the ability for a user to Self-service defer them selves to a slower ring. This will put them at a later install date than they may have had when getting added to one of your deployment rings, and therefore giving them some more time to finalize a project or perhaps get back from a holiday without getting faced by a new operating system version.

What’s fun about this solution is that it leverages both the Graph API and the Microsoft Intune Powershell functions from the Intune team (found on GitHub), Microsoft Intune Software Update Rings, Azure Automation and Azure AD cmdlets – as well as self service group management (even if its not required) using Azure AD Premium. So you’ll put your EMS licenses to good use, with minimum effort.

So, first ill give you a brief overview about how the script works from a functionality point of view, and then ill explain some of the logic around the script.

Pre-reqs and functionality

This script assumes that you in some way have applied Software Update rings to some or all of your users or Windows 10 devices. This can be done partly using my other script that I blogged about last week: First tool in the toolbox – Create Software Update rings and groups in Intune

What’s missing is a script to actually populate the rings, but I’m working on that as we speak and will post it shortly. But you are of course free to populate your rings in the way you choose.

Next, I recommend that you setup Self-service group management in Azure AD – so that your users can add and remove themselves from the group. The script will of course work in the exact same way if you add or remove users manually or by script.

In my case, I’m using the property “FacsimileTelephoneNumber” in the User Azure AD objects to tattoo the group the user was a member of when he or she (and their devices) got deferred. If you are using this property for something else, it should be fine to replace that value with another one.

Lastly, you should configure a schedule for the script to run on from Azure Automation.

Optional: If the user isn’t a member of any Azure AD Group that are used for Windows Servicing, you could add a default group to put the user in when its removed from the deferral group. This is done using a variable in Azure Automation, not surprisingly name “DefaultGroup”. What you need to add to active this function is the GroupID for the group where you want to add your users. If you don’t enter this value, they’ll just get removed from the deferral group.

DefaultGroup.JPG

Next, a brief overview around what the script will do. This is the scenario its built for – (if you have other’s that you would like me to support, let me know!):

Azure AD Groups are populated with users and/or devices. These AAD groups are assigned to a number of different Windows 10 Software Update rings that applies settings for how feature- and quality upgrades and updates are applied. In my case I have two pilot rings, two deployment rings and two deferral groups, where one is used for Self-Service deferral.

When a user (or you as an administrator) would like to temporarily defer a user to a later install date the user object is added to the corresponding Azure AD Group. When the script runs, and you can choose your schedule based on the size of your environment and requirements,  it will look into the Self-Service deferral group and get all the user members from it.

The script will take note of the Azure AD Groups the users belongs to and add this info to the user object for later use.

It will then remove the user and all devices owned by that user from all other pilot- and deployment rings – and add the user and all its devices to the deferral group. This ensure that you don’t get challenges with more than one policy being applied to the user or its devices. The downside being that this today only supports deferral of the user and all its devices and not individual devices (if that’s a requirement).

When the user is OK with getting the newest upgrade or update, the object should be removed from the Self-Service deferral group and the next time the script runs – it will re-add the user and its devices to the group that were added to the user object.

The reason for all this is to keep the user and admin-experience as simple as possible. All you need to do is to add or remove user objects from a specific Azure AD Group, and its also enables self-service by either the included feature in Azure AD Premium or any other Self-Service tool that supports adding users to Azure AD Groups, or even synchronized AD groups, though I haven’t tested that scenario. Later on ill do another script that supports ConfigMgr and Hybrid scenarios (both Co-management and Hybrid AD Join) but that’s in the roadmap.

Now, lets get into the script:

Script and logic

You’ll find the script on GitHub as well as at the bottom of this post.

As with my first tool, this tools uses a number of Functions from the Intune team built on the Graph API – as well as Nickolaj Andersens Powershell module to get the authentication token. These are imported and added before starting the script that then authenticates to both Intune and Azure AD.

Script_Part1

We then set a number of variable from Azure Automation as well as create a new object that we later will use in a function to compare membership in groups. Lastly we’ll gather a number of variables from Azure AD to be able to find groups, users and devices. When all that’s done, the rest of the script is divided into three parts:

  • Gather information of users and their devices as well as enable re-enablement of deployment rings.
  • Deferral of users and devices.
  • Re-enablement of users and devices.

Script_Part2.JPG

In the first part of the script, we’ll start by checking of the user is a member of any of the pilot or deployment rings. If the user is a member, we’ll tattoo the name of that group in to the FacsimileTelephoneNumber property of that user, to be used later on. If the user is a member of more than one group (which they shouldn’t be, but stuff happends :)) the script will choose the first group that gets returned – and if the user aren’t a member of any group – the first half of the first part of the script wont be used. Lastly, the user itself will be removed from the pilot- or deployment groups of which they are members.

Next, we’ll get all devices that are owned by this user. I’m using the AzureAD cmdlet for this rather than the Intune one as I feel that I get more information from AzureAD than from Intune as this stage. However, the challenge is that the AzureAD cmdlet (or the Azure AD object probably) can have more than one user – and with the Intune function it only returns one. Later on, ill match the result to get ONE user, but that’s something to have in mind. I need to look into it later on.

All the (Windows) devices that have the user as owner will be added to a variable. They are then checked to see if they already are member of the deferral group, and if they are not, they’ll be added. That concludes the first part of the script.

Script_Part3.JPG

The second part of the script is the part that does the actual deferral – all the devices from all the deferred users are checked to see if they are member of any of the pilot- or deployment groups. If they are, they’ll be removed from these groups.

Script_Part4.JPG

The third and last part of the script (which is also the largest part) is all about restoring users and devices when they no longer need to be deferred. It will look at all the devices in the group and compare them to the deferred devices (based on the users of the group). So, to be restored, you first need to remove the user object from the group. The script will then compare the members to the variable of deferred devices and remove the devices that for any reason are deferred but don’t have there owner in the group.

Based of the owners value in FacsimileTelephoneNumber they’ll be either restored to that group or to a default group that you may set as a variable. If no default group has been set, the devices will just be removed. As for the user (or the Owner), the it or them will also be restored to the group based on the same conditions as the devices. In that way, you can have a user-centric approach to Windows Servicing – instead of looking at particular devices.

Script_Part5.JPG

There may be scenarios when you would like to defer individual machines. This is currently not supported by this script – but instead (when using my first tool that creates the groups and profiles) a second group and profile are created to manually (or later on integrated with Upgrade Readiness, but again – its a LATER story) add individual machines to.

I do consider the script functional, but there are still things to work on. Ill do my best to improve performance and error-handling, but I welcome of kinds of feedback. The next step will be to create a script that populates WaaS-groups based on the information provided by Intune – and later also (as stated above) a integration to WA and Upgrade Readiness.

Try it out (at your own risk of course :)) and let me know if you have any feedback or feature requests!


#Author: Simon Binder
#Blog: bindertech.se
#Twitter: @Bindertech
#Thanks to: @daltondhcp, @davefalkus, @NickolajA and the Intune product group.
#Most functions is copied from the Powershell Intune Samples: https://github.com/microsoftgraph/powershell-intune-samples
#Script requires the Azure AD Powershell module or the Azure AD Preview Powershell module to run

# Import required modules
try {
  Import-Module -Name AzureAD -ErrorAction Stop
  Import-Module -Name PSIntuneAuth -ErrorAction Stop
}
catch {
  Write-Warning -Message "Failed to import modules"
}

function Get-AuthToken {

  

  [cmdletbinding()]

  param
  (
    [Parameter(Mandatory=$true)]
    $User
  )

  $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User

  $tenant = $userUpn.Host

  Write-Host "Checking for AzureAD module..."

  $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-host
    write-host "AzureAD Powershell module not installed..." -f Red
    write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow
    write-host "Script can't continue..." -f Red
    write-host
    exit
  }

  # Getting path to ActiveDirectory Assemblies
  # If the module count is greater than 1 find the latest version

  if($AadModule.count -gt 1){

    $Latest_Version = ($AadModule | select version | Sort-Object)[-1]

    $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }

    # Checking if there are multiple versions of the same module found

    if($AadModule.count -gt 1){

      $aadModule = $AadModule | select -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

  $clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"

  $redirectUri = "urn:ietf:wg:oauth:2.0:oob"

  $resourceAppIdURI = "https://graph.microsoft.com"

  $authority = "https://login.microsoftonline.com/$Tenant"

  try {

    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

    # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
    # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession

    $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"

    $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")

    $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result

    # If the accesstoken is valid then create the authentication header

    if($authResult.AccessToken){

      # Creating header for Authorization token

      $authHeader = @{
        'Content-Type'='application/json'
        'Authorization'="Bearer " + $authResult.AccessToken
        'ExpiresOn'=$authResult.ExpiresOn
      }

      return $authHeader

    }

    else {

      Write-Host
      Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
      Write-Host
      break

    }

  }

  catch {

    write-host $_.Exception.Message -f Red
    write-host $_.Exception.ItemName -f Red
    write-host
    break

  }

}

####################################################

Function Get-AADDevice(){

  

  [cmdletbinding()]

  param
  (
    $DeviceID
  )

  # Defining Variables
  $graphApiVersion = "v1.0"
  $Resource = "devices"

  try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$DeviceID'"

    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value 

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

Function Get-ManagedDevices(){

  

  [cmdletbinding()]

  param
  (
    [switch]$IncludeEAS,
    [switch]$ExcludeMDM
  )

  # Defining Variables
  $graphApiVersion = "beta"
  $Resource = "deviceManagement/managedDevices"

  try {

    $Count_Params = 0

    if($IncludeEAS.IsPresent){ $Count_Params++ }
    if($ExcludeMDM.IsPresent){ $Count_Params++ }

    if($Count_Params -gt 1){

      write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function"
      Write-Host
      break

    }

    elseif($IncludeEAS){

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"

    }

    elseif($ExcludeMDM){

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'"

    }

    else {

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'"
      Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices"
      Write-Host

    }

    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

Function Get-ManagedDeviceUser(){

  

  [cmdletbinding()]

  param
  (
    [Parameter(Mandatory=$true,HelpMessage="DeviceID (guid) for the device on must be specified:")]
    $DeviceID
  )

  # Defining Variables
  $graphApiVersion = "beta"
  $Resource = "deviceManagement/manageddevices('$DeviceID')?`$select=userId"

  try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
    Write-Verbose $uri
    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).userId

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

# Read credentials and variables

$AADCredential = Get-AutomationPSCredential -Name "WaaSAccount"
$Credential = Get-AutomationPSCredential -Name "WaaSAccount"
$AppClientID = Get-AutomationVariable -Name "AppClientID"
$WindowsVersion = Get-AutomationVariable -Name "WindowsVersion"
$Tenantname = Get-AutomationVariable -Name "Tenantname"
$DefaultGroup = Get-AutomationVariable -Name "DefaultGroup"
$Groups = New-object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck

# Acquire authentication token
try {
    Write-Output -InputObject "Attempting to retrieve authentication token"
    $AuthToken = Get-MSIntuneAuthToken -TenantName $Tenantname -ClientID $AppClientID -Credential $Credential
    if ($AuthToken -ne $null) {
        Write-Output -InputObject "Successfully retrieved authentication token"
    }
}
catch [System.Exception] {
    Write-Warning -Message "Failed to retrieve authentication token"
}

Connect-AzureAD -Credential $AADCredential

#Sets variable for WaaS-groups, Device & User Members of the Self-Service group.

#Sets the Self-Service Group in Script.

$group = Get-AzureADGroup -SearchString "$WindowsVersion-Self-Service Deferred" | Select-Object -ExpandProperty ObjectID
$SearchGroups = Get-AzureADGroup -SearchString "$WindowsVersion-SAC"
$WaaSGroups = Get-AzureADGroup -SearchString "$WindowsVersion-SAC" | Select-Object -ExpandProperty ObjectID
$DeviceMembers = Get-AzureADGroupMember -ObjectId $group -All:$True | Where-Object {$_.ObjectType -eq 'Device'}
$users = Get-AzureADGroupMember -ObjectId $group -All:$True | Where-Object {$_.ObjectType -eq 'User'}  | Get-AzureADUser | Select-Object -ExpandProperty UserPrincipalName 

#Gets all Windows-devices that have each user-member as owner.
#If they already are members they will be skipt and if not the devices will be added to the group

foreach ($user in $users){

  $Groups.GroupIds = $WaaSGroups
  $GroupID = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $user -GroupIdsForMembershipCheck $Groups

  if ($GroupID -ne $null) {
                           $TatooMember = Get-AzureADGroup | Where-Object ObjectID -eq $GroupID | Select-Object -First 1 -ExpandProperty DisplayName
                           Set-AzureADUser -ObjectId $User -FacsimileTelephoneNumber $Tatoomember

        foreach ($UniqueGroupID in $GroupID) {
                                              $MemberRing = Get-AzureADGroup | Where-Object ObjectID -eq $UniqueGroupID | Select-Object -ExpandProperty ObjectID
                                              $MemberUser = Get-AzureADUser -Filter "userPrincipalName eq '$User'" | Select-Object -ExpandProperty ObjectID
                                              Remove-AzureADGroupMember -ObjectId $MemberRing -MemberId $MemberUser
        }

  }

  $devices = Get-AzureADUserOwnedDevice -ObjectId $user | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

  foreach ($device in $devices){  

    if ($DeviceMembers -match $device){

      ('{0} is already a member of the group' -f $Device)

    }

    Else{ 

      Add-AzureADGroupMember -ObjectId $group -RefObjectId $device 

    }

  } 

}

#Gets all deferred devices from users in the group and check if they are members of any other WaaS-group. If so, they are removed.

$deferreddevices = $Users | ForEach-Object{

  Get-AzureADUserOwnedDevice -ObjectId $user | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

}

foreach ($deferreddevice in $deferreddevices){

  foreach ($WaaSGroup in $WaaSGroups){

    $WaaSMember = Get-AzureADGroupMember -ObjectId $WaaSgroup -All:$True | Where-Object {$_.ObjectType -eq 'Device'} | Select-Object -ExpandProperty ObjectID

    if ($deferreddevice -in $WaaSMember){

      Remove-AzureADGroupMember -ObjectId $WaaSGroup -MemberId $deferreddevice
    }

  }
}

#Gets all devices in the group and compares to the deferred devices. If a device is member, but not have its user in the group, its removed. Also re-adds the user to any previous deployment- or pilot-ring.

foreach ($DeviceMember in $DeviceMembers){

        $MemberObjectID = ($Devicemember | Select-Object -ExpandProperty ObjectID ) 

  if ($MemberObjectID -notin $deferreddevices){

    Remove-AzureADGroupMember -ObjectId $group -MemberId $MemberObjectID

    #If you want to re-add previously deferred machines to a specific group enable the lines below

        $DeviceID = $DeviceMember | Select-Object -ExpandProperty DeviceID
        $IntuneDevice = Get-ManagedDevices -IncludeEAS | Where-Object -Property AzureADDeviceID -EQ $DeviceID  | Select-Object -First 1 -ExpandProperty id
        $OwnerID = Get-ManagedDeviceUser -DeviceID $IntuneDevice
        $Owner = Get-AzureADUser -ObjectID $OwnerID
        $GroupName = $Owner | Select-Object -ExpandProperty FacsimileTelephoneNumber

        if ($GroupName -eq $null) {

                                    $IntuneOwner = Get-AzureADDeviceRegisteredOwner -ObjectId $MemberobjectID | Where-Object -Property UserPrincipalName -NE $Owner.UserPrincipalName
                                    $MultipleOwners = $IntuneOwner.FacsimileTelephoneNumber

                                    if ($MultipleOwners -ne $null) {

                                                $RestoreGroup = Get-AzureADGroup -SearchString $MultipleOwners | Select-Object -ExpandProperty ObjectID
                                                $Groups.GroupIds = $RestoreGroup
                                                $CheckGroup = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $IntuneOwner.UserPrincipalName -GroupIdsForMembershipCheck $Groups

                                                    if ($CheckGroup -eq $Null) {
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $IntuneOwner.ObjectId
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                                               }
                                                    else {
                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                          }
                                                          }

                                    else {

                                            if ($DefaultGroup -ne $Null) {
                                                "No previous membership detected, assigning device and owner to default group $DefaultGroup"
                                                 Add-AzureADGroupMember -ObjectId $DefaultGroup -RefObjectId $MemberobjectID
                                                 Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $Owner
                                                 }
                                            else {
                                                'No default restore group assigned, device will be removed from deferral group'
                                                 }
                                                 }
                                    }

        else {

                                                $RestoreGroup = Get-AzureADGroup -SearchString $GroupName | Select-Object -ExpandProperty ObjectID
                                                $Groups.GroupIds = $RestoreGroup
                                                $CheckGroup = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $Owner.UserPrincipalName -GroupIdsForMembershipCheck $Groups

                                                    if ($CheckGroup -eq $Null) {
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $Owner.ObjectId
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                                                }

                                                                                else {
                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                          }

        }

  }

}

 

As a Solution Architect, Simon inspires customers, partners and colleagues to create the best possible workplace for their users. His main focus is the Windows platform – but todays workplace consists of so much more than that. As an MCT he is passionate about teaching and sharing knowledge. He’s a frequent speaker, blogger and podcaster – as well as a penguin fanatic.

Tagged with: , , , , , , , , , , , , , , , ,
Posted in Azure, Education, Intune, Microsoft, Windows 10, Windows as a Service
One comment on “The second – and updated – tool in the WaaS Toolbox – Self-Service Deferral
  1. […] purpose etc. Also, this ensure a consistent user experience for the user. In the same way as my second tool (Self-Service deferral) deferred all devices when a user were added to the group, the same inclusion applies […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

I’m speaking!
I’m going!
Follow me on Twitter!
%d bloggers like this: