Monthly Archives: April 2014

See you at TechEd in Houston!

In just two weeks it’s time for TechEd North America in Houston. We will both attend this year, and Johan will spend quite a few hours as a Technical Learning Guide in the HOL/ILL area.
If you’re there and happen to see any of us, stop by and say hi! 🙂

/ Johan & Andreas

See-You-There

Office 365: Assign licenses based on groups using PowerShell – Advanced version

Important note: The end of an era with licensing scripts is near… and the beginning of a new one with Azure AD Group Based Licensing is here. Group Based Licensing is now in preview and currently requires a paid Azure AD Subscription. Try it out and give Microsoft your feedback on how they can make it even better! 

The never ending story about Office 365 licensing continues… This time it’s an extension of my script to assign licenses based on groups, with additional functionality to remove and change licenses for users.
I’ve come across scenarios where this have been a requirement a couple of times, and wanted to see how much work that was required to get the job done. 🙂

If you just want to assign licenses for users based on groups, plain and simple, this is not the script for you…

Running the script
The script is tested in a tenant with two different license types (E1 and E3). Therefore, the functionality has been verified against that, so if you have three or four different licenses to assign in your tenant, you have to do your own testing! 🙂

The following functions are included in the script:

  • Assignment of licenses for new users based on groups/licenseSKU’s in the $licenses hashtable
  • Switch of licensetype if a user is moved from one group to another
  • Removal of license if the user no longer is a member in any of the license assignment groups

LicenseFix

IMPORTANT: Since the script actually will remove licenses for users that are not in any of the groups, you have to make sure that you populate the license assignment groups prior to first time running the script.

Apart from the above, the script requirements and setup details are the same as in this post.

LicenseO365Users.ps1

<#   .SYNOPSIS      Script that assigns Office 365 licenses based on Group membership in WAAD. .DESCRIPTION     The script assigns of licenses for new users based on groups/licenseSKUs in the $licenses hashtable.     It switch licensetype if a user is moved from one group to Another.     It removes the license if the user no longer is a member in any of the license assignment Groups.     Updated 2015-03-25 to support multiple skus for each user.     The script REQUIRES PowerShell 3.0 or later! .NOTES      Author: Johan Dahlbom      Blog: 365lab.net      Email: johan[at]dahlbom.eu      The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.      #>
#Import Required PowerShell Modules
Import-Module MSOnline

#Office 365 Admin Credentials
$CloudUsername = 'admin@365lab.net'
$CloudPassword = ConvertTo-SecureString 'Password' -AsPlainText -Force
$CloudCred = New-Object System.Management.Automation.PSCredential $CloudUsername, $CloudPassword

#Connect to Office 365
Connect-MsolService -Credential $CloudCred

$Licenses = @{
                 'E1' = @{
                          LicenseSKU = 'mstlabs:STANDARDPACK'
                          Group = 'E1_Users'
                        }                        

                 'E3' = @{
                          LicenseSKU = 'mstlabs:ENTERPRISEPACK'
                          Group = 'E3_Users'
                        }
            }

$UsageLocation = 'SE'

#Get all currently licensed users and put them in a custom object
$LicensedUserDetails = Get-MsolUser -All | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object {
 [pscustomobject]@{
            UserPrincipalName = $_.UserPrincipalName
            License = $_.Licenses.AccountSkuId
            }
 }

#Create array for users to change or delete
$UsersToChangeOrDelete = @()

foreach ($license in $Licenses.Keys) {

  #Get current group name and ObjectID from Hashtable
  $GroupName = $Licenses[$license].Group
  $GroupID = (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId
  $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}

  Write-Output "Checking for unlicensed $license users in group $GroupName with ObjectGuid $GroupID..."
  #Get all members of the group in current scope
  $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress
  #Get all already licensed users in current scope
  $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName
  $UsersToHandle = ''

    if ($GroupMembers) {
        if ($ActiveUsers) {
            #Compare $Groupmembers and $Activeusers
            #Users which are in the group but not licensed, will be added
            #Users licensed, but not, will be evaluated for deletion or change of license
            $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject             $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject
        } else {
            #No licenses currently assigned for the license in scope, assign licenses to all group members.
            $UsersToAdd = $GroupMembers
        }

    } else {
      Write-Warning  "Group $GroupName is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)"
      #If no users are a member in the group, add them for deletion or change of license.
      $UsersToChangeOrDelete += $ActiveUsers
    }

  #Check the amount of licenses left...
  if ($AccountSKU.ActiveUnits - $AccountSKU.consumedunits -lt $UsersToAdd.Count) {
        Write-Warning 'Not enough licenses for all users, please remove user licenses or buy more licenses'
  }

     foreach ($User in $UsersToAdd){

        #Process all users for license assignment, if not already licensed with the SKU in order.
          if ((Get-MsolUser -UserPrincipalName $User).Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) {
            try {
                  #Assign UsageLocation and License.
                  Set-MsolUser -UserPrincipalName $User -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
                  Set-MsolUserLicense -UserPrincipalName $User -AddLicenses $AccountSKU.AccountSkuId -ErrorAction Stop -WarningAction Stop
                  Write-Output "SUCCESS: Licensed $User with $license"
            } catch {
                  Write-Warning "Error when licensing $User"

            }

          }
     }
}

#Process users for change or deletion
if ($UsersToChangeOrDelete -ne $null) {
        foreach ($User in $UsersToChangeOrDelete) {
          if ($user -ne $null) {

            #Fetch users old license for later usage
            $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License

             #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object.
             #Only one license group per user is currently supported.
             $ChangeLicense = $Licenses.Keys | ForEach-Object {
                  $GroupName = $Licenses[$_].Group
                  if (Get-MsolGroupMember -All -GroupObjectId (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId | Where-Object {$_.EmailAddress -eq $User}) {
                     [pscustomobject]@{
                        OldLicense = $OldLicense
                        NewLicense = $Licenses[$_].LicenseSKU
                     }
                  } 

              }

              if ($ChangeLicense) {
                    #The user were assigned to another group, switch license to the new one.
                    try {
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)"
                    } catch {
                          Write-Warning "Error when changing license on $User`r`n$_"
                    }

              } else {  

                    #The user is no longer a member of any license group, remove license
                    Write-Warning "$User is not a member of any group, license will be removed... "
                    try {
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Removed $OldLicense for $User"
                    } catch {
                          Write-Warning "Error when removing license on user`r`n$_"
                    }
              }
         }
    }
}

Hope this helps you if having this scenario, please let me know if you have features requests or other things that can improve the script!

/Johan

Conditional mail routing in Exchange Online Protection

In this post we will go through how to achieve conditional mail routing in Exchange Online Protection, without the need of having forwarding addresses per user. This means we can deliver incoming emails sent to the same domain to different back end servers, based on other criterias than forwarding addresses, like certain attributes or group membership.

If interested in how to do this using the mail users and forwarding addresses method, Andreas wrote a post a while ago about using Exchange Online as a mail gateway for decentalized email domains.

Scenario:

  • Exchange Online Protection is in place for all incoming emails.
  • You have a single Exchange organization, but different servers across the globe. You want to have external incoming emails directly to the correct mailbox server without transferring them trough your WAN connection.
  • DirSync from your tenant to EOP is already activated and in place, which means all users and Groups are already in WAAD.
  • Our incoming email routing will look like in the sketch below, based on AD Group membership.

Setup

As we in this case already have decided to base our email routing on AD groups which is already in sync with WAAD, the things we need to configure in EOP for each target server are following:

1. Create an outbound connector with support for Criteria Based Routing (CBR). Of course more than one smart host can be added for redundancy purposes. cbr

New-OutboundConnector -Name "CBR - eumx.365lab.net" `
                      -ConnectorType OnPremises `
                      -SmartHosts "eumx.365lab.net" `
                      -UseMXRecord $false `
                      -IsTransportRuleScoped $true 

2. Create a mail flow/transport rule for each destination connector/ad group.cbr-transport

New-TransportRule -Name "CBR - eumx.365lab.net" ` 
                  -SentToMemberOf "Europe@365lab.net" `
                  -RouteMessageOutboundConnector "CBR - eumx.365lab.net"

To avoid mail flow issues for users that are not in any of the groups, make sure you keep your existing default outbound connector.

/Johan

PowerShell GUI application to set thumbnailPhoto

A while ago I started experimenting with writing GUI applications in PowerShell. This script that updates the thumbnailPhoto attribute of a user was my first attempt, and since it works quite well I thought that I might share it with you.

The thumbnailPhoto attribute is used more and more, and it is now supported by Outlook, SharePoint and Lync. It is also supported by Office 365, and when using DirSync the data is synced to Windows Azure Active Directory.

This script uses Windows Presentation Foundation (WPF) which is part of .Net 3.0. The window form is easily built with Microsoft Visual Studio Express 2013 for Windows Desktop and can be saved as a XAML document, which we can import in our PowerShell script.

Data are saved and retrieved from Active Directory using ADSI, which means no additional PowerShell modules must be installed. The only requirements are PowerShell 2.0 or newer with .Net Framework 3.0. If running PowerShell version 2.0 you also need the -STA parameter to powershell.exe.

The application itself is very simple, just type the username in the search box, and browse for a photo. When clicking Apply the image data is saved to Active Directory. By default all users has permission to change their own thumbnailPhoto. To change other users, permissions needs to be delegated.

There is a limit in picture size, maximum size is 100 kB, but since Office 365 cannot handle that large image I have set the limit in the script to 10 kB. Also recommended resolution is 96×96 pixels.

thumbnailphotoguiapp

Here’s the full script, please note that you need a valid path to the .xaml document.

Set-ThumbnailPhotoGUI-mainform.xaml

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Thumbnail photo changer app" Height="350" Width="525" ResizeMode="NoResize">
    <Grid x:Name="list1" Margin="0,0,0,2">
        <Label Content="Username:" HorizontalAlignment="Left" Width="105" Margin="20,23,0,250" />
        <TextBox x:Name="txtSearch" Margin="138,27,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Width="122"/>
        <Button x:Name="btnSearch" Content="Search" HorizontalAlignment="Left" VerticalAlignment="Top" Width="50" Margin="271,27,0,0"/>
        <Label Content="Surname:" HorizontalAlignment="Left" Height="30" Margin="20,92,0,0" VerticalAlignment="Top" Width="92"/>
        <Label x:Name="lblSn" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,92,0,0"/>
        <Label Content="UserPrincipalName:" HorizontalAlignment="Left" Height="30" Margin="20,123,0,0" VerticalAlignment="Top" Width="115"/>
        <Label x:Name="lblUPN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="346" Margin="134,123,0,0"/>
        <Label Content="Givenname:" HorizontalAlignment="Left" Height="30" Margin="20,61,0,0" VerticalAlignment="Top" Width="92"/>
        <Label x:Name="lblGivenName" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,61,0,0"/>
        <Image x:Name="imgThumb" HorizontalAlignment="Left" Height="96" Margin="29,203,0,0" VerticalAlignment="Top" Width="96"/>
        <Button Name="btnImg" Content="Change photo..." HorizontalAlignment="Left" Height="28" Margin="149,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
        <Label Content="DistinguishedName:" HorizontalAlignment="Left" Height="30" Margin="20,156,0,0" VerticalAlignment="Top" Width="126"/>
        <Label x:Name="lblDN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="373" Margin="134,156,0,0"/>
        <Button x:Name="btnApply" Content="Apply" HorizontalAlignment="Left" Height="28" Margin="278,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
    </Grid>
</Window>

Set-ThumbnailPhotoGUI.ps1

if ([threading.thread]::CurrentThread.ApartmentState.ToString() -eq 'MTA') {
    Write-Error "This script requires PowerShell to run in Single-Threaded Apartment. Please run powershell.exe with parameter -STA"
    Exit
}

Add-Type -AssemblyName PresentationFramework 
[xml]$XAML = Get-Content "Set-ThumbnailPhotoGUI-mainform.xaml"
$XAML.Window.RemoveAttribute("x:Class")
 
$global:userinfo = @{
    dn = $null
    photo = [byte[]]$null
}
 
function Invoke-FileBrowser {
    param([string]$Title,[string]$Directory,[string]$Filter="jpeg (*.jpg)|*.jpg")
    [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog
    $FileBrowser.InitialDirectory = $Directory
    $FileBrowser.Filter = $Filter
    $FileBrowser.Title = $Title
    $Show = $FileBrowser.ShowDialog()
    If ($Show -eq "OK") { Return $FileBrowser.FileName }
}
 
function Get-LDAPUsers($sam) {
    $activeDomain = New-Object DirectoryServices.DirectoryEntry
    $domain = $activeDomain.distinguishedName
    $searcher = [System.DirectoryServices.DirectorySearcher]"[ADSI]LDAP://$domain"
    $searcher.filter = "(&(samaccountname=$sam)(objectClass=user)(objectClass=person))"
    $result = $searcher.findall()
     
    if ($result.count -gt 1) {
        Write-Warning "More than one user found. Please refine your search."
    } elseif ($result.count -eq 1) {
        $result = $searcher.findone().getDirectoryEntry()
        [pscustomobject]@{
            UserPrincipalName = $result.userprincipalname[0]
            DistinguishedName = $result.distinguishedname[0]
            samAccountName = $result.samaccountname[0]
            Givenname = $result.givenname[0]
            Surname = $result.sn[0]
            ThumbnailPhoto = $result.thumbnailphoto
        }
    }
}
 
function Update-WPFForm($sam) {
   $selecteduser = Get-LDAPUsers $sam
   if ($selecteduser) {
        $global:userinfo.dn = $selecteduser.DistinguishedName
        $global:userinfo.photo = [byte[]]$($selecteduser.thumbnailphoto)
 
        $lblGivenName.Content = $selecteduser.GivenName
        $lblSN.Content = $selecteduser.SurName
        $lblUPN.Content = $selecteduser.UserPrincipalName
        $lblDN.Content = $global:userinfo.dn
        $txtSearch.Text = $selecteduser.SamAccountName
        $imgThumb.Source = $global:userinfo.photo
        $btnImg.IsEnabled = $true
    }
}
 
#region GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $window=[Windows.Markup.XamlReader]::Load($reader)
 
    $btnSearch = $window.FindName("btnSearch")
    $btnImg = $window.FindName("btnImg")
    $btnApply = $window.FindName("btnApply")
    $txtSearch = $window.FindName("txtSearch")
    $lblGivenName = $window.FindName("lblGivenName")
    $lblSn = $window.FindName("lblSn")
    $lblUPN = $window.FindName("lblUPN")
    $lblDN = $window.FindName("lblDN")
    $imgThumb = $window.FindName("imgThumb")
#endregion GUI
 
#region Events
 
    #region btnSearchClick
        $btnSearch_click = $btnSearch.add_Click
        $btnSearch_click.Invoke({
            Update-WPFForm($txtSearch.Text)
        })
    #endregion btnSearchClick
 
    #region btnImgClick
        $btnImg_click = $btnImg.add_Click
        $btnImg_click.Invoke({
            $file = Invoke-FileBrowser -Title "Choose a thumbnail photo" -Filter "Jpeg pictures (*.jpg)|*.jpg"
            if ($file) {
                if ((Get-Item $file).length -gt 10kB) {
                    Write-Warning "Picture too large. Maximum size is 10 kB."
                } else {
                    $global:userinfo.photo = [byte[]](Get-Content $file -encoding byte)
                    $imgThumb.Source = $global:userinfo.photo
                    $btnApply.IsEnabled = $true
                }
            }
        })
    #endregion btnImgClick
     
    #region btnApplyClick
        $btnApply_click = $btnApply.add_Click
        $btnApply_click.Invoke({
            $user = New-Object DirectoryServices.DirectoryEntry "LDAP://$($global:userinfo.dn)"
            $user.put("thumbnailPhoto", $global:userinfo.photo)
            $user.setinfo()
            $btnApply.IsEnabled = $false
            Write-Verbose "User $($global:userinfo.dn) updated."
        })
    #endregion btnApplyClick
     
    #region textChange
        $txtSearch_Change = $txtSearch.add_KeyDown
        $txtSearch_Change.Invoke({
            if ($_.Key -ieq 'Return') {
                Update-WPFForm($txtSearch.Text)
            }
        })
    #endregion textChange
 
#endregion Events
 
$window.ShowDialog() | Out-Null

This is just an example how a useful script with GUI can be written with quite simple means. Now let’s get the camera and take some photos!

/ Andreas

Office 365: Migrate from Cloud identities to Dirsync

In some scenarios, you may have to transfer the the source of authority for your already cloud-only created user accounts in Office 365. One scenario when this is required, is when changing from cloud only identities to synchronized identities from your local Active Directory. Another scenario is when you migrate your AD users from one domain to another, as described in this post.

To make the transitioning to DirSync as smooth as possible, you have to take a couple of precautions to make sure that existing Office 365 accounts are properly matched with the accounts in your local Active Directory.

This can be done using one of the following two methods:

  • Soft matching of each user. For a soft match to properly work, you need to make sure that the primary email address (both in mail and in proxyaddresses) and userprincipalname in the cloud and in the local Active Directory matches.
  • For the users that are to be matched, construct an ImmutableID for each user in the local AD and synchronize that value to WAAD prior activation and doing the first Directory Synchronization. The ImmutableID is basically a Base64-encoded value of the ObjectGuid attribute.

Solution
To make this transition easier, I have created a script that and copies the required attributes from WAAD to your local Active Directory automatically. The script can use both methods to match the users.

If you run the script without any parameters, the following attributes will be updated for all matching users in your local Active Directory:

  • GivenName
  • sn (SurName)
  • DisplayName
  • mail
  • proxyAddresses

If using the -UpdateImmutableID switch, an ImmutableID will be created for the user and synced back to WAAD. Please note, that process will only work without additional steps if you have not been using DirSync before.

With the -CreateNewUsers switch, you have the possibility to let the script create users that are in WAAD but not in your local AD. The SamAccountname will be “firstname.lastname” and a random password will be generated for the new user.

2014-04-18 20-53-32
2014-04-18 20-56-44
2014-04-18 20-57-41

Note: a pre-requisite for the script to work properly is that you’ve already populated the UserPrincipalName attribute for all users in your local Active directory. Of course this must match with the UPN’s used in Office 365/WAAD.

SyncFrom365toAD.ps1

<#  
.SYNOPSIS 
    Script that synchronizes attributes from users in WAAD to the local active directory.
.PARAMETER UpdateImmutableID 
    Constructs and inserts the ImmutableID attribute in WAAD.
.PARAMETER TargetOU
    OU to create new users in.
.PARAMETER CreateNewUsers
    Creates new users for users that are not matching the WAAD users UPN.
.NOTES 
    Author: Johan Dahlbom 
    Blog: 365lab.net 
    Email: johan[at]dahlbom.eu 
    The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.     
#>
param(
[parameter(Mandatory=$false)]
[switch]$UpdateImmutableID = $false,
[parameter(Mandatory=$false)]
[switch]$CreateNewUsers = $false,
[parameter(Mandatory=$false)]
[string]$TargetOU = "OU=Users,OU=365Lab,DC=cloud,DC=lab"
)
 
#Load required Modules and Assemblys
Import-Module MSOnline
Import-Module ActiveDirectory
[Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
 
Connect-MsolService
 
$WAADUsers = Get-MsolUser -All | Where-Object {$_.UserPrincipalName -notlike "*.onmicrosoft.com"}
 
foreach ($WAADUser in $WAADUsers) {
    $ADuser = Get-ADUser -Filter {UserPrincipalName -eq $WAADUser.UserPrincipalName}
      
    try {
 
        $Mail = $($WAADUser.ProxyAddresses | Where-Object {$_ -cmatch "^SMTP"}).substring(5)
         
        if ($ADuser) {
        #Update AD user with attributes from WAAD
        Set-ADUser -Identity $ADuser `
            -EmailAddress $Mail `
            -GivenName $WAADUser.FirstName `
            -SurName $WAADUser.LastName `
            -DisplayName $WAADUser.DisplayName `
            -Replace @{ProxyAddresses=[string[]]$WAADUser.ProxyAddresses} `
            -ErrorAction Stop -WarningAction Stop
 
        Write-Output "SUCCESS: Updated $($ADuser.userprincipalname) with proper attributes from WAAD"
         
        } else {
 
            if ($CreateNewUsers) {
 
                     #Generate password for the user aduser
                     $NewUserPassword = [System.Web.Security.Membership]::GeneratePassword(13,0)
 
                     #Generate samaccountname "firstname.lastname"
                     $SAM = [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes("$($WAADUser.Firstname).$($WAADUser.Lastname)".ToLower()))
                                           
                     New-ADUser -Path $TargetOU `
                        -Name $WAADUser.DisplayName `
                        -UserPrincipalName $WAADUser.UserPrincipalName `
                        -SamAccountName $sam `
                        -Enabled $true `
                        -EmailAddress $Mail `
                        -GivenName $WAADUser.FirstName `
                        -SurName $WAADUser.LastName `
                        -DisplayName $WAADUser.DisplayName `
                        -OtherAttributes @{ProxyAddresses=[string[]]$WAADUser.ProxyAddresses} `
                        -AccountPassword (ConvertTo-SecureString $NewUserPassword -AsPlainText -force) `
                        -ErrorAction Stop -WarningAction Stop
 
                     Write-Output "SUCCESS: Created user $sam with password $NewUserPassword"

            } else {
                    
                     Write-Warning "User $($WAADUser.UserPrincipalName) is not in AD, user might not be synchronized correctly or become a duplicate user.`r`nCreate the user manually or run the script with -CreateNewUsers switch."
            
            }
        }
               
        #If UpdateImmutableID switch has been used, construct and set ImmutableID for the WAAD user.
        if ($UpdateImmutableID) {
            $ADuser = Get-ADUser -Filter {UserPrincipalName -eq $WAADUser.UserPrincipalName}
            if ($ADuser) {
                $ImmutableID = [system.convert]::ToBase64String($ADuser.ObjectGUID.ToByteArray())
                Set-MsolUser -UserPrincipalName $WAADUser.UserPrincipalName -ImmutableId $ImmutableID -ErrorAction Stop -WarningAction Stop
            }
        }
      
   } catch [Microsoft.Online.Administration.Automation.MicrosoftOnlineException] {
             
            Write-Warning "Error when updating ImmutableID $ImmutableID for $Mail`r`n$_"
 
   } catch {
            
            Write-Warning "Error when creating user $Mail`r`n$_"
        
   }
 
}

Good luck running the script and with your Dirsync implementation!

/Johan

Office 365: Assign licenses based on groups using PowerShell

Important note: The end of an era with licensing scripts is near… and the beginning of a new one with Azure AD Group Based Licensing is here. Group Based Licensing is now in preview and currently requires a paid Azure AD Subscription. Try it out and give Microsoft your feedback on how they can make it even better! 

Update 2016-01-07: Updated the script with a new function to support nested groups.

As a follow up to my earlier post about assigning Office 365 licenses based on ad attribute, I’ve now created another script that uses another approach to assign the licenses.
Instead of using ad attributes, we are here using security groups to assign the licenses.

Since you want to automate license assignment, I am assuming that you are using DirSync.
In this script, the groups and group members are enumerated directly in Windows Azure Active Directory. This means that the groups you will use to assign licenses, must be synchronized to WAAD. If you are looking for a more advanced script with change and removal functions, check this one out.

Note: Users that already have a license are not processed by the script, nor are licenses removed from users that are not longer member of the groups.

Getting started
Getting started with the script is very easy. In the $Licenses hash table, you define the licenses to assign, the group to use for assignment and the license SKU.
The license SKU id’s for your tenant are found with the Get-MsolLicenseSKU cmdlet, as below:

2014-04-15 22-10-25

Here you will find a good translation table that will help you interpret the License SKU ID to the actual license type. When you have done that, just to put in your O365 credentials and UsageLocation in the script and then you’re all set to schedule the script.

2014-04-15 22-37-53

LicenseO365Users.ps1

#2016-01-07 - Updated the script with a new function to support nested groups.
#Import Required PowerShell Modules
#Note - the Script Requires PowerShell 3.0!
Import-Module MSOnline

#Office 365 Admin Credentials
$CloudUsername = 'admin@tenant.onmicrosoft.com'
$CloudPassword = ConvertTo-SecureString 'password' -AsPlainText -Force
$CloudCred = New-Object System.Management.Automation.PSCredential $CloudUsername, $CloudPassword

#Connect to Office 365
Connect-MsolService -Credential $CloudCred
function Get-JDMsolGroupMember {
    param(
        [CmdletBinding(SupportsShouldProcess=$true)]
        [Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Position=0)]
        [ValidateScript({Get-MsolGroup -ObjectId $_})]
        $ObjectId,
        [switch]$Recursive
    )
    begin {
        $MSOLAccountSku = Get-MsolAccountSku -ErrorAction Ignore -WarningAction Ignore
        if (-not($MSOLAccountSku)) {
            throw "Not connected to Azure AD, run Connect-MsolService"
        }
    }
    process {
        $UserMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes User -All
        if ($PSBoundParameters['Recursive']) {
            $GroupsMembers = Get-MsolGroupMember -GroupObjectId $ObjectId -MemberObjectTypes Group -All
            $GroupsMembers | ForEach-Object -Process {
                $UserMembers += Get-JDMsolGroupMember -Recursive -ObjectId $_.ObjectId
            }
        }
        Write-Output ($UserMembers | Sort-Object -Property EmailAddress -Unique) 

    }
    end {

    }
}

$Licenses = @{
                 'E1' = @{
                          LicenseSKU = 'mstlabs:STANDARDPACK'
                          Group = 'E1_Users'
                        }                           

                 'E3' = @{
                          LicenseSKU = 'mstlabs:ENTERPRISEPACK'
                          Group = 'Office 365 E5'
                        }
            }
$UsageLocation = 'SE'

foreach ($license in $Licenses.Keys) {

    $GroupName = $Licenses[$license].Group
    $GroupID = (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId
    $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}

    Write-Output "Checking for unlicensed $license users in group $GroupName with ObjectGuid $GroupID..."

    $GroupMembers = (Get-JDMsolGroupMember -ObjectId $GroupID -Recursive | Where-Object {$_.IsLicensed -eq $false}).EmailAddress

    if ($AccountSKU.ActiveUnits - $AccountSKU.consumedunits -lt $GroupMembers.Count) {
        Write-Warning 'Not enough licenses for all users, please remove user licenses or buy more licenses'
      }

        foreach ($User in $GroupMembers) {
          Try {
            Set-MsolUser -UserPrincipalName $User -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
            Set-MsolUserLicense -UserPrincipalName $User -AddLicenses $AccountSKU.AccountSkuId -ErrorAction Stop -WarningAction Stop
            Write-Output "Successfully licensed $User with $license"
          } catch {
            Write-Warning "Error when licensing $User`r`n$_"
          }

        }

}

Looking for other script options using groups?
Office 365: Assign licenses based on groups using PowerShell – Advanced version – adds and removes licenses for the users as well.
Office 365: Assign individual parts of licenses based on groups using PowerShell – script with the ability to add and remove serviceparts of a license.

There are lots of things that are not in the script that could also be automated, please help us prioritize with the features you’d like to have the most.

/Johan

Using Exchange Online as mail gateway for decentralized email domains

In some scenarios it is useful to have a central email server that forwards messages to different email servers. You might for example have a company with different business units, each having an email server of their own. In this post I will show you how to get started with the mail flow using one domain, but with several different email servers in Exchange Online.

Consider the following scenario. Several email servers exists within a company:

  • In USA an on-premises Exchange environment is using @us.contoso.com addresses
  • In Spain an external hosting provider is delivering an email service based on pop3 using @contosospain.com addresses
  • In Poland Exchange Online is used, but with @contoso.pl addresses

A new corporate policy states that all users must have an email address of the form givenname.surname@contoso.com. Also, a central antispam system must be used. At this point no consolidation of email servers will be done. Exchange Online Protection licenses has been bought by the company.

To take care of the mail flow in Exchange Online we have to create our Office 365 tenant and add the contoso.com domain to it. We also have to change our DNS and let the MX records for contoso.com point to Exchange Online. This will give us the following mail flow for a user in for example Poland:

Mail flow

I have a csv file with all users in each location:

aliasfile

For each user we will create a mail enabled user that holds the alias and target email addresses. I will use PowerShell to create these users:

#Import all users
$users = Import-Csv users.csv

#Create a password for all users
$Password = ConvertTo-SecureString 'Pa$$w0rd' -asPlainText -Force

#Create mail-enabled user
ForEach ($user in $users) {
	$Name = $user.Name
	$Email = $user.Alias
	$Target = $user.Email
	
	New-MailUser -Name $Name -MicrosoftOnlineServicesID $Email -ExternalEmailAddress $Target -Password $Password
}

Now all emails sent to the @contoso.com addresses will be delivered to the current regional email server. The next step would be to configure each email server to use @contoso.com as from address, and only accept incoming emails from Exchange Online.

/ Andreas

Lync Online: “The user is not found or has not yet been provisioned for Lync Online”

This is an issue that I’ve been seeing a couple of times, but a couple of weeks a go, a nice colleague of mine helped me find a more sustainable solution in that particular case.

Issue:
When you are using Dirsync to synchronize your users from your local AD to WAAD, quite a lot of attributes follow your synchronized objects to WAAD.
Having a Lync Server on premise, your Lync activated users has some 7 attributes starting with msRTCSIP-, as below:

    • msRTCSIP-ApplicationOptions
    • msRTCSIP-DeploymentLocator
    • msRTCSIP-Line
    • msRTCSIP-OwnerUrn
    • msRTCSIP-PrimaryUserAddress
    • msRTCSIP-UserEnabled
    • msRTCSIP-OptionFlags

The problem with these attributes are, that they prevent your Lync Online users to be provisioned.
The error you will get when trying to verify settings on a particular lync user, is “The user is not found or has not yet been provisioned for Lync Online. Please verify that the user exists, wait 60 minutes, and then try again. If the problem continues, contact Microsoft Customer Service and Support.”

Solutions:

Option 1. Setup your Lync Server as a Hybrid deployment, as described here.
The problem in my particular case was that the Lync deployment the customer had, did not include an edge server, which is required for a hybrid deployment.

Option 2. Clear the msRTCSIP-* attributes for all Lync users on premise.
This is an option that works very well in smaller environments where you are not required to do some testing before rolling a solution out. 🙂 Since you’re clearing the attributes, Lync for the affected users will stop working on premise.
Clearing the attributes for ALL Lync enabled users on premise, can easily be done with the following PowerShell line using the ActiveDirectory module:

Get-ADUser -Filter {proxyaddresses -like "sip:*"} | 
  Set-ADUser -Clear  msRTCSIP-ApplicationOptions,msRTCSIP-DeploymentLocator,msRTCSIP-Line,msRTCSIP-OwnerUrn,msRTCSIP-PrimaryUserAddress,msRTCSIP-UserEnabled,msRTCSIP-OptionFlags

After clearing the attributes, you first need to force a synchronization of Dirsync, and then you do of course need to repoint your Lync related dns records to Office 365.

Option 3. Exclude the msRTCSIP-* attributes from the Directory Synchronization.
This is something that is needed if you want to try out Lync Online for a couple of users before moving them all to Lync Online.
Note: this requires some ‘major’ Changes in the Synchronization service that Dirsync runs on. Do the steps with caution!

Here is how to do it:
1. Start the Synchronization Service Manager (miisclient.exe) on the DirSync server. In the “Management agents” pane, select properties on the Active Directory connector.

2. In the management agent designer, select “Configure attribute flow” and remove the msRTCSIP for both the User and inetOrgPerson Data Source from the attribute flow as below. Note that you can only remove one attribute at a time.


3. If you have not yet made your first Directory synchronization, you’re now all set to to do that.
If not (which probably is the case since you found this post 🙂 ), you need to to clear the Active Directory Connector space before the problem will be solved. This operation would be equivalent dangerous to installing a new DirSync server in the same domain, e.g. no worries at all.
You will not lose any custom settings as OU or custom attributes that you are using to set your DirSync scope.

Deleting the Active Directory connector space is done with steps as below (it’s safe to click delete):

After the connector space has been deleted, run a Full Dirsync again, wait around 30 minutes and hopefully your Lync Online users have now been provisioned properly.

Hope this helps you if running in to this problem, and remember that it’s always more than one solution to a problem!

/Johan