Example for using classes in PowerShell – New-Progressbar

PowerShell is a fairly easy language to learn, but to some extent, creating classes is a bit of a hidden concept. The way to do it is to wrap functions in a module and use Export-ModuleMember to make them externally visible. Here is an easy to understand example of implementing the Write-Progress cmdlet in an even easier way.


function New-Progressbar
{
    [CmdletBinding()]
    Param
    (
        # Total count
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   Position=0)]
        [int]$TotalCount,

        # Activity name
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=1)]
        [string]$ActivityName = "Running",

        # Time estimation
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=2)]
        [boolean]$TimeEstimationEnabled = $true
    )

    # Create new module instance
    $m =  New-Module -ScriptBlock {
        # Internal variables
        $script:total = 1;
        $script:current = 0;
        $script:ActivityName = " ";
        $script:startTime = Get-Date;
        $script:timeEstimation = $false;
        # Functions with obvious method names
        function setActivityName($name) {$script:ActivityName = $name}
        function setTotal($tot) { $script:total = $tot}
        function getTotal($tot) { return $script:total}
        function enableTimeEstimation() {$script:timeEstimation = $true}
        function disableTimeEstimation() {$script:timeEstimation = $false}


        # Progress the progressbar one step. Optional parameter Text for defining the status message
        function Progress {
            Param
            (
                [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$false,
                    Position=0)]
                [string]$Text = ("{0}/{1}" -f $script:current, $script:total)
            )

            $params = @{
                Activity = $script:ActivityName
                Status = $Text
                PercentComplete = ($script:current / $script:total * 100)
            }

            if($script:timeEstimation) {
                if($script:current -gt 5) {
                    $params["SecondsRemaining"] = (((Get-Date) - $script:startTime).TotalSeconds / $script:current) * ($script:total - $script:current)
                }
            }

            Write-Progress @params

            if($script:current -lt $script:total) {
                $script:current += 1
            } else {
                Write-Warning "Progressbar incremented too far"
            }
        }
        function Complete() {Write-Progress -Activity $script:ActivityName -Status $script:total -PercentComplete 100 -Completed}
        export-modulemember -function setTotal,getTotal,Progress,Complete,setActivityName,enableTimeEstimation,disableTimeEstimation
    } -AsCustomObject

    # Set initial values
    $m.setTotal($TotalCount)
    $m.setActivityName($ActivityName)

    if($TimeEstimationEnabled) {
        $m.enableTimeEstimation()
    }

    return $m;
}

To use it, simply copy and paste the code into any PowerShell. After doing this, you have a new cmdlet available – New-Progressbar. You can use Get-Help New-ProgressBar -Full to get example usage, but here is a quick example script:

# Get a big list of files of unknown size
$files = @(dir $env:SystemRoot | select -First (Get-Random -Minimum 10 -Maximum 100))

# Create progressbar
$bar = New-Progressbar -TotalCount $files.Count -ActivityName "Processing files"

# Pretend to process files in order to progress the progressbar
$files | foreach {
    $bar.Progress($_.Name)
    sleep -Milliseconds 100
}

# Complete the progressbar
$bar.Complete()

As you can see, $bar is here a custom class with methods presented through the module. You can have internal methods too; only the methods exported by Export-ModuleMember is available externally.

Hope this helps someone as a quick reference to creating a PowerShell module.

Using PowerShell to determine which mailboxes are actually in use

Here is a PowerShell script I’ve created which give you a csv file containing all the information you should need for determining which mailboxes are in use or not. I have primarily used it to determine which shared mailboxes are no longer active and should be deleted. The script supports both Exchange on-premises and Exchange Online.

When connecting to on-premises Exchange, the script requires the Exchange admin tools. This means you can run it from your Exchange server, or any other computer with this installed. If you use Exchange Online, there is no such requirement.

In both cases, the Active Directory PowerShell module is required.


function Create-MailboxReport {
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$False,Position=0)]
        [string]$CSV = "report.csv",

        [Parameter(Mandatory=$False,Position=1)]
        [int]$MaxResults = 10000,

        [Parameter(Mandatory=$false)]
        $ADFilter = {msExchRecipientTypeDetails -eq 2},

        [Parameter(Mandatory=$false)]
        [bool] $ExchangeOnline = $false,

        [Parameter(Mandatory=$false)]
        [ValidateSet("sAMAccountName", "userPrincipalName", "DistinguishedName", "mailNickname")]
        $ExchangeIdentifierAttribute = "sAMAccountName"
    )

    # Different approach between on-premises Exchange and Exchange Online
    if($ExchangeOnline) {
        # If Connect-ExchangeServer is available, we are in an Exchange PowerShell. That means we cannot load the Exchange Online cmdlets.
        if(Get-Command "Connect-ExchangeServer" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) {
            Write-Error "Sorry, you are running an Exchange PowerShell and trying to connect to Exchange Online. Please open a regular PowerShell."
            return;
        }

        # If Get-Mailbox already exists, there is not reason to connect again.
        if(!(Get-Command "Get-Mailbox" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) {
            Write-Verbose "Connecting to Exchange Online"
            $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Authentication Basic -AllowRedirection -Credential (Get-Credential)
            Import-PSSession $session -DisableNameChecking
        } else {
            Write-Verbose "Already connected to Exchange Online?"
        }
    } else {
        # Load RemoteExchange if Connect-ExchangeServer is not present
        if(!(Get-Command "Connect-ExchangeServer" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) {
            Write-Verbose "Loading RemoteExchange"
            $remoteExchange = 'C:\Program Files\Microsoft\Exchange Server\V14\Bin\RemoteExchange.ps1'
            if(Test-Path $remoteExchange) {
                . $remoteExchange
            } else {
                Write-Error "Could not find $remoteExchange"
                return;
            }
        }

        Write-Verbose "Connecting to Exchange on-premises"
        Connect-ExchangeServer -Auto
    }

    Write-Verbose "Loading AD module"
    Import-Module ActiveDirectory -Verbose:$false

    Write-Verbose ("Getting max {0} users from AD matching filter: {1}" -f $MaxResults, $ADFilter)
    Write-Progress -Activity "Getting user objects from AD" -Status " " -PercentComplete 20
    $adusers = Get-ADUser -filter $ADFilter -Properties lastlogontimestamp,whencreated,DisplayName,altRecipient,msExchHideFromAddressLists,Manager,msExchDelegateListLink,mailNickname -ResultSetSize $MaxResults
    Write-Progress -Activity "Getting user objects from AD" -Status " " -PercentComplete 100 -Completed

    $inc = 1;
    $adusers | foreach{
        $AD = $_ # This makes it a bit more easy to read
        Write-Progress -Activity "Running" -Status ("{0}/{1} - {2}" -f $inc, $adusers.Count, $AD.SamAccountName) -PercentComplete ($inc / $adusers.Count * 100) ; $inc++
        Write-Debug "Getting mailbox statitics"

        # Get mailbox statistics for mailbox. If it fails, give warning but continue with the rest of the mailboxes.
        $MAILBOXSTATISTICS = Get-MailboxStatistics -Identity $AD.$ExchangeIdentifierAttribute -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
        if(!$MAILBOXSTATISTICS) {
            Write-Warning ("Could not find mailbox statics for {0}" -f $AD.$ExchangeIdentifierAttribute )
            return;
        }

        # Get all mailboxe permissions that are not inherited, that is not on the form "NT AUTHORITY\SELF" and the SID is resolvable
        Write-Debug "Getting mailbox permissions"
        $MAILBOXPERMISSION = Get-MailboxPermission -Identity $AD.$ExchangeIdentifierAttribute | where{!$_.IsInherited} | where{([string]$_.User) -notlike "NT AUTH*"} | where{([string]$_.User) -notlike "S-1-5-21-*"}
        Write-Debug ("Found {0} mailbox permissions" -f ($MAILBOXPERMISSION | measure).Count)

        # Extract a few attributes
        $lastlogontimestamp = if($AD.lastlogontimestamp){$AD.lastlogontimestamp}else{0}


        # Create hashmap with all properties
        $properties = @{
            DisplayName = $AD.DisplayName
            sAMAccountName = $AD.SamAccountName
            MailboxItemCount = $MAILBOXSTATISTICS.ItemCount
            MailboxTotalItemSize = $MAILBOXSTATISTICS.TotalItemSize
            MailboxLastAccessedTime = $MAILBOXSTATISTICS.LastLogonTime
            ADObjectWhenCreated = $AD.whencreated
            DistinguishedName = $AD.DistinguishedName
            HiddenFromAddressListsEnabled = $AD.msExchHideFromAddressLists
            ForwardedTo = $AD.altRecipient
            LastLoggedOnUserAccount = $MAILBOXSTATISTICS.LastLoggedOnUserAccount
            Manager = $AD.Manager
            NumberOfAutomappings = ($AD.msExchDelegateListLink | measure).Count
            NumberOfDelegations = ($MAILBOXPERMISSION | measure).Count
            ADObjectLastLogonTimeStamp = [datetime]::FromFileTime($lastlogontimestamp)
        }

        # Create custom object
        return New-Object -TypeName PSObject -Property $properties
    } | Export-Csv -Path $CSV -Encoding UTF8 -NoTypeInformation -Delimiter ";"

    Write-Progress -Activity "Running" -Status "Completed" -PercentComplete 100 -Completed
    Write-Output "$CSV created"
}

To use this script, just take the above script and paste it into PowerShell on the computer you want to run it from. This will define a new cmdlet, Create-MailboxReport, with several options. Here is some ways you can use it.

Create a report for the 10 first AD user objects that have the mail attribute, but not employeeid. The MaxResults paramter can be usefull when testing out a bigger report.
Create-MailboxReport -ADFilter {employeeid -notlike "*" -and mail -like "*"} -MaxResults 10

Create a report for the 10000 first mailboxes that you have in Exchange Online. (Takes many hours)
Create-MailboxReport -ADFilter {targetaddress -like "*.onmicrosoft.com"} -ExchangeOnline:$true -ExchangeIdentifierAttribute userPrincipalName

Create a report for the 10000 first mailboxes in office 365 where the on-premises samaccountname starts with MBX
Create-MailboxReport -ADFilter {targetaddress -like "*.onmicrosoft.com" -and samaccountname -like "MBX*"} -ExchangeOnline:$true -ExchangeIdentifierAttribute userPrincipalName

Create a report for all on-premises mailbox
Create-MailboxReport -ADFilter {homemdb -like "*"} -MaxResults 10000000

Then with the csv report, what i usually do is to open this in Excel and create filters on the columns. Here you can for example do things like the following.

  1. Find all shared mailboxes that have not been accessed the last 2 years (filter on LastAccessTime)
  2. Find all shared mailboxes that no-one has access to (filter on NumberOfDelegations)
  3. Find user accounts that have not been logged into for 6 months (filter on ADLastLogonTimestamp

Hope this saves someone a bit of time!

Playing around with the AADSync beta

The beta for the new AADSync tool has been released, and I have been playing around with it a little to discover whats in store and what we might expect from Microsoft Identity Manager (FIM vNext) when that comes out. This is a screenshot heave “try it for the first time and screenshot everything”-article. What I am trying is to see what the new rule editor can manage, and to see whether I can actually manage to provision users in a separate AD forest directly from AADSync.

I made it through the regular installation, which you can see documented somewhat here. This yielded a quite usual DirSync setup, but in miisclient.exe you cannot see attribute flows and filters. This is instead move to the “Synchronization Rules Editor”.

I started of creating a secondary AD management agent. The first ting i notice here is the amount of management agents available by default. This is in strong contrast to DirSync.







Not much to comment on in these screenshots, except for the fact that you cannot configure filters and attribute flow, everything is still FIM. New MA added:

Having a look at the default configure inbound and outbound rules in the new “Synchronization Rules Editor”.

I clicked “Add new rule”, and started on my inbound join rule for my “external.goodworkaround.com” AD, which is my second forest. I just want to see if I can provision user account into that forest based on the users in my “gwrnd.goodworkaround.com” primary forest.

Some new filtering possibilities available, that needed an extension before. However, there is no way of extending it yourself it seems.

Joining pager in AD with sourceAnchor in FIM.

I’ll just skip adding inbound attribute flow for now.

Added successfully.

The default list of outbound sync rules.

Trying to create a outbound provisioning rule to my “external.goodworkaround.com” forest.

Filtering.

Same join as the inbound rule.

Let’s just try some transformation and see if it works on first try!

It did.

Doing a full synchronization on the primary AD yields 71 adds to my second AD.

Looking good.

Seems right (wonder if I am missing some very important attribute?)

Exported successfully

Some changes not reimported (userAccountControl was wrong value)

I forgot the sAMAccountName too.

Just as before.

Some still giving error, due to sAMAccountName being more than 20 characters.

Let’s see if “Left” still exists.

Yep.

One more thing. The new way of triggering “DirSync” is like below. Also notice that it is automatically triggering my custom added MA!

Looks good (except that it did not Delta sync on my custom MA, only import and export).

That’s it. As I said, just some screenshots to see what’s in store. I am starting to look forward to Microsoft Identity Manager / FIM vNext; hopefully this will be a part of it.

PowerShell script to find duplicate proxyaddresses

This script uses the Active Directory PowerShell module to locate duplicate proxyaddresses throughout your forest. The script must be run from a computer that have the AD PowerShell installed, and can reach all PDCEmulators in all domains in your forest.

Import-Module ActiveDirectory

# Create hashmap for proxyaddresses
$proxyaddresses = @{}

# For each domain in the forest
Get-ADForest | Select-Object -ExpandProperty Domains | Get-ADDomain | foreach {
    Write-Output ("Parsing domain {0} by contacting {1}" -f $_.Name, $_.PDCEmulator)
    # Get all AD objects that have proxyaddresses
    Get-ADObject -Filter {proxyaddresses -like "*"} -Properties proxyaddresses -Server $_.PDCEmulator | foreach {
        $_.proxyAddresses | foreach { $proxyaddresses[$_] += 1}
    }
}

Write-Output "Done, looking for duplicates"
$duplicates = $proxyaddresses.Keys | where{$proxyaddresses[$_] -gt 1}


# Output proxyaddresses that are duplicates
if($duplicates) {
    Write-Output "The following proxyaddresses was found multiple times"
    $duplicates # | Out-Gridview # Remove first hash-sign in order to get an "Excel"-view. Needs PowerShell ISE.
} else {
    Write-Output "No duplicates found"
}

PowerShell script to enable litigation hold for all user mailboxes in Exchange

The following PowerShell script will enable litigation hold for all user mailboxes in your environment. The script will work for Exchange 2010 and 2013, both On-Premise or Online.

# First you need to be connected to the Exchange PowerShell.

$pagesize = 100; # The number of mailboxes per loop
$inc = 0; # Start increment value

# Continue until all mailboxes are litigation hold enabled
do {
    Write-Output "Getting mailboxes"
    # Get UserMailboxes that does not have litigation hold enabled
    $mailboxes = Get-Mailbox -Filter {LitigationHoldEnabled -eq $false -and RecipientTypeDetails -eq "UserMailbox"} -ResultSize $pagesize -WarningAction SilentlyContinue
    if($mailboxes) { Write-Output ("Current mailbox count: {0}" -f ($inc += $mailboxes.Count))}
    # Enable litigation hold
    $mailboxes | Set-Mailbox -LitigationHoldEnabled $true -WarningAction SilentlyContinue
} while($mailboxes);

Write-Output "Done"

It is possible to add additional filtering by editing the filter for the Get-Mailbox cmdlet.

Howto – ADFS on Windows Server 2012 R2 with Office 365

Windows Server 2012 R2 is RTM and published on MSDN. Active Directory Federation Services (ADFS) 3.0 have some major differences from the 2012 version (ADFS 2.1). The ADFS Proxy is gone, replaced by the Web Application Proxy (WAP), a part of the Remote Access role. The WAP is an replacement for the ADFS proxy and can also be used to publish other applications such as SharePoint and Outlook Web Access. Also Bring Your Own Device (BYOD) is possible with the new Device Registration Service / Workplace Join and there are better support for two-factor authentication. IIS is no longer used, which removes a lot of the customization possibilities. This article is about how to configure the new ADFS for Office 365.

Let’s get right to it. My setup is a non-redundant system with a single ADFS server and a single Web Application Proxy (WAP). My federation server farm will in this case be called sts.goodworkaround.com. From this guide it should be easy to extrapolate how to set up the service in a load balanced and highly available cluster.

In earlier versions of ADFS, it was recommended to install it on dedicated servers. This was partly because ADFS ran inside IIS and required quite a lot of resources. Since ADFS no longer reqiures IIS, there is much less overhead, and you can now install ADFS on your domain controllers. Keep in mind that you cannot use Windows Network Load Balancing (NLB) with DCs, and must use an external load balancer if you require high availability in this configuration.

Start by installing the Active Directory Federation Services role with Server Manager.


After completing, click “Configure the federation service on this server”. In the new window that opens, choose default option which is “Create the first federation server in a federation server farm”. Even though you do not plan to create a farm, even a single server is still a farm and you can at any time extend your one-server farm to a bigger farm by adding additional nodes.

Choose the account used for the action. I recommend using a Domain Administrator, but you can get away with using an account by granting write permissions to a container in AD.

After choosing the account, it’s time to import the certificate and setting the federation server farm name. The certificate can be a wildcard (like mine), or with a subject. The federation server farm name must match the subject of the certificate. The certificate must be 2048-bit or more.

Click import and type the password for the pfx file. If you do not have the certificate available as a pfx container, you must convert it to this format. An easy way is to use IIS on a different host and import and export it from there.

After importing the certificate, depending on the certificate type, choose a federation server name and a display name. The display name can be changed at a later stage, but the federation server name can not.

On the specify service account page, there are two options. The first option is to create a group managed service account (GMSA). This option requires at least one Windows Server 2012 domain controller in the domain where the GMSA is created. The GMSA option is the easiest and automates both password management and servicePrincipalName. The second option is to use an already existing GMSA or a regular user account. The wizard will automatically configure SPN for the user, if it has permissions. The user will automatically be granted the necessary permissions.

Next, choose the type of configuration database. Here too there are two options. The default option is using Windows Integrated Database (WID). WID is mostly the same as SQL Server Express, and to my experience, this is sufficient for most customers and actually recommended when the federation server farm contains less than 8 servers. If installing more than 8 servers in the federation server farm, choose SQL instead. Note that there are some security features that requires SQL server.

Next yourself through the following screens.



Voila! You have deployed your first ADFS server. Now let’s continue configuring DNS and testing the server.

Start by creating a A-record (Do not use a cname record, as this will give you errors with windows authentication / single sign-on!) that points to the server’s IP address. If you are doing a highly available setup, point the A record to your load balancing IP address.

Now, use the url https://sts.goodworkaround.com/adfs/ls/IdpInitiatedSignon.aspx to test your federation server. Start by testing from a domain user on a domain joined client. You should see something like the following.

If you have added the site as a Intranet Site in security settings, you should get Single Sign-On (SSO) and get directly to the following.

If your browser do not trust the site or do not support Windows authentication, you will be requested for credentials.

Now that the ADFS server is tested, let’s continue on and installing the Web Application Proxy (WAP). The WAP will be used to present ADFS to the internet. You can use any kind of reverse proxy to do this, such as TMG or F5. The WAP server requires to resolve your federation server name to your federation server. It is best practice to have the server running the WAP standalone and not part of the same domain as the ADFS servers. This because the WAP server is directly presented to the internet.

WAP is new in 2012 R2 and is a part of the Remote Access role. Use Server Manager to install the Remote Access role.

Next, next, next to the Role Features page and choose Web Application Proxy.

Start the Web Application Proxy wizard.

In the wizard, read the text and click next.

Type in the federation server name and administrator credentials. These credentials will only be used once in order to create a proxy trust, and they are not stored.

Import and choose the same certificate as on your ADFS server. This can also be different, but that’s just one more certificate to keep valid.

Hit “configure” and you are done.

After finishing the wizard, the Remote Access Management console will automatically start. Create a new pass-through publishing by clicking publish in the right menu.

Fill in your information. The external and internal url must be the same.

Now you have published ADFS. What you now must do is to make the WAP accessible from the internet and point the federation server name in public/internet DNS to the WAP. In my case this means pointing sts.goodworkaround.com to the WAP server.

Now you should verify that https://sts.goodworkaround.com/adfs/ls/IdpInitiatedSignon.aspx is available and working from the internet.

Federating Office 365

Your ADFS setup is now ready to be used with Office 365. You can follow the same guides for configuring your ADFS with 2012 R2 as with 2008 R2 and 2012. Anyway, here is how.

Start by installing the Microsoft Online Services Sign-In Assistant and the Microsoft Online Services Sign-In Assistant. You can find instructions for this here at TechNet. It can be a good idea to install this on the ADFS server, but you can also do this from other computers as long as they can contact the ADFS server.

After installing these two components, on the ADFS server start a PowerShell as administrator and type the cmdlet Import-Module MsOnline. After successfully importing the module, continue with Connect-MsolService. You will be asked for credentials. Type a global administrator i Office 365.

If running the PowerShell on another computer, use the Set-MsolADFSContext cmdlet to point to the ADFS server.

In order to federate domains, they must be verified in the Office 365 portal. You can check this with the Get-MsolDomain cmdlet. This should show the domain as verified.

If the domain is verified, use the Convert-MsolDomainToFederated with the DomainName parameter to federate a domain. You should also use the SupportMultipleDomains parameter, or you will not be able to use the same ADFS servers to federate other domains in the same tenant. Here is the whole PowerShell.

Import-Module MsOnline
Connect-MsolService
Get-MsolDomain
Convert-MsolDomainToFederated -DomainName goodworkaround.com -SupportMultipleDomain:$true

Testing Office 365 sign-on

After you have successfully federated the domain, you are ready to test signing into Office 365 via federation. In order for this to work you should already have set up Windows Azure Directory Synchronization Tool, and your UPN suffixes should match federated domains.

No matter whether a user have an Exchange Online license assigned or not, we can test federation by going to http://outlook.com/goodworkaround.com. This should now automatically redirect to your ADFS server. If you use this URL externally, you will get to ADFS through WAP. If you use this URL internally, you should get to ADFS directly, and possibly have single sign-on. Log on with a upn such as marius@goodworkaround.com and you should be redirected to Outlook Web Access (if you have no license, you should get an error from OWA that you do not have a mailbox).

It is also a good idea to test the proxy by testing other sign-on options than web redirect. My suggestion is that you grant a user Global Administrator in the Office 365 portal and try to sign into the Microsoft Online PowerShell with the UPN marius@goodworkaround.com and the password.

Hope this helps someone! If you have any questions, use the comment section below. I have done this so many times that there might be some steps I forget to mention.

FIM – Clear runs from PowerShell

FIM provides a Windows Management Interface (WMI) namespace for working with the synchronization service. This is root\MicrosoftIdentityIntegrationServer, and contains a couple of classes. One of them is MIIS_SERVER. This can be used to clear runs. This must be run as a user with either membership in the FIMAdmins or the FIMSyncOperators group.

# Get the MIIS_SERVER class
$lstSrv = @(get-wmiobject -class "MIIS_SERVER" -namespace "root\MicrosoftIdentityIntegrationServer" -computer ".")
# Clear runs older than 3 days
$date = (get-date).addDays(-3)
# Create string in correct format (yyyy-MM-ddTH:m:s.000)
$datestr = $date.year.ToString()+"-"+$date.month.ToString().PadLeft(2,"0")+"-"+$date.day.ToString().PadLeft(2,"0")+"T"+$date.hour.ToString().PadLeft(2,"0")+":00:00.000"
# Run the query
$lstSrv[0].ClearRuns($datestr)