Documenting with PowerShell: Documenting Office365 mailbox permissions

I like being able to report to our clients exactly what the permissions on mailboxes are. The only issue with reporting via the Office 365 admin portal is that we don’t get a history. I’ve decided to make sure that I can do this by documenting the permissions daily.

To do this, I have two versions of the script; one for IT-Glue and one for general HTML file usage. Both scripts connect to the Office365 Exchange PowerShell using the Secure App Model. For more information you can check this blog post.

IT-Glue version

The IT-Glue version gets all contacts in your IT-Glue environment, compares these to the domains found in each of your partner tenants and documents the permissions for the right tenant into the correct IT-Glue client. The script creates a Flexible Asset for you if it does not yet exist.

$key                   = "YOUR IT Glue Key here"
$ApplicationId         = 'Application ID'
$ApplicationSecret     = 'Application Secret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'YourRefreshTokens'
$ExchangeRefreshToken  = 'YourExchangeRefresherToken'
$upn                   = 'UPN-That-Generated-Tokens'
$APIEndpoint           = "https://api.eu.itglue.com"
$FlexAssetName         = "Office365 Permissions - Automatic Documentation"
#######################################################################
write-output "Getting Module"
If (Get-Module -ListAvailable -Name "ITGlueAPI") { Import-module ITGlueAPI } Else { install-module ITGlueAPI -Force; import-module ITGlueAPI }
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Add-ITGlueBaseURI -base_uri $APIEndpoint
Add-ITGlueAPIKey $key
write-output "Getting IT-Glue contact list"
$i = 0
do {
    $AllITGlueContacts += (Get-ITGlueContacts -page_size 1000 -page_number $i).data.attributes
    $i++
    Write-Host "Retrieved $($AllITGlueContacts.count) Contacts" -ForegroundColor Yellow
}while ($AllITGlueContacts.count % 1000 -eq 0 -and $AllITGlueContacts.count -ne 0) 
Write-Output "Checking if flexible asset exists."
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if (!$FilterID) { 
Write-Output "No Flexible Asset found. Creating new one"
    $NewFlexAssetData = 
@{
    type = 'flexible-asset-types'
    attributes = @{
            name = $FlexAssetName
            icon = 'sitemap'
            description = $description
    }
    relationships = @{
        "flexible-asset-fields" = @{
            data = @(
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order           = 1
                        name            = "tenantid"
                        kind            = "Text"
                        required        = $true
                        "show-in-list"  = $true
                        "use-for-title" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 2
                        name           = "permissions"
                        kind           = "Textbox"
                        required       = $false
                        "show-in-list" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 3
                        name           = "tenant-name"
                        kind           = "Text"
                        required       = $false
                        "show-in-list" = $false
                    }
                }
            )
            }
        }
          
   }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
}


write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
   
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname

    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "
Permissions on $($mailbox.PrimarySmtpAddress)
" | Out-String }
    }
    Remove-PSSession $session

    $FlexAssetBody =
    @{
        type       = 'flexible-assets'
        attributes = @{
            traits = @{
                'permissions' = $HTMLPermissions
                'tenantid'    = $MSOLtentantID
                'tenant-name' = $initialdomain.name
            }
        }
    }


    write-output "             Finding $($customer.name) in IT-Glue"
    $orgID = @()
    foreach ($customerDomain in $customerdomains) {
        $orgID += ($AllITGlueContacts | Where-Object { $_.'contact-emails'.value -match $customerDomain.name }).'organization-id' | Select-Object -Unique

    }



    write-output "             Uploading Office Permission $($customer.name) into IT-Glue"
    foreach ($org in $orgID) {
        $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $($filterID.ID) -filter_organization_id $org).data | Where-Object { $_.attributes.traits.'tenant-name' -eq $initialdomain.name }
        #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
        if (!$ExistingFlexAsset) {
            $FlexAssetBody.attributes.add('organization-id', $org)
            $FlexAssetBody.attributes.add('flexible-asset-type-id', $($filterID.ID))
            write-output "                      Creating new Office Permission $($customer.name) into IT-Glue organisation $org"
            New-ITGlueFlexibleAssets -data $FlexAssetBody
        }
        else {
            write-output "                      Updating Office Permission $($customer.name) into IT-Glue organisation $org"
            $ExistingFlexAsset = $ExistingFlexAsset[-1]
            Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody
        }

    }
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null 
}

General HTML version

The generic HTML version makes a HTML file per client in C:\Temp. It uses the name of the client as the filename.

$ApplicationId         = 'ApplicationID'
$ApplicationSecret     = 'ApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'YourUPN'
#######################################################################
write-output "Getting Modules"
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
$head = @"
<script>
function myFunction() {
    const filter = document.querySelector('#myInput').value.toUpperCase();
    const trs = document.querySelectorAll('table tr:not(.header)');
    trs.forEach(tr => tr.style.display = [...tr.children].find(td => td.innerHTML.toUpperCase().includes(filter)) ? '' : 'none');
  }</script>
<title>Audit Log Report</title>
<style>
body { background-color:#E5E4E2;
      font-family:Monospace;
      font-size:10pt; }
td, th { border:0px solid black; 
        border-collapse:collapse;
        white-space:pre; }
th { color:white;
    background-color:black; }
table, tr, td, th {
     padding: 2px; 
     margin: 0px;
     white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px; }
h2 {
font-family:Tahoma;
color:#6D7B8D;
}
.footer 
{ color:green; 
 margin-left:10px; 
 font-family:Tahoma;
 font-size:8pt;
 font-style:italic;
}
#myInput {
  background-image: url('https://www.w3schools.com/css/searchicon.png'); /* Add a search icon to input */
  background-position: 10px 12px; /* Position the search icon */
  background-repeat: no-repeat; /* Do not repeat the icon image */
  width: 50%; /* Full-width */
  font-size: 16px; /* Increase font-size */
  padding: 12px 20px 12px 40px; /* Add some padding */
  border: 1px solid #ddd; /* Add a grey border */
  margin-bottom: 12px; /* Add some space below the input */
}
</style>
"@
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null
    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "<h4>Permissions on $($mailbox.PrimarySmtpAddress)</h4>" | Out-String }
    }
    Remove-PSSession $session
    $CompleteHTML = $head,$HTMLPermissions | Out-String | out-file "C:\Temp\$($customer.name).html"
} 

14 Comments

  1. Euan Stewart November 26, 2019 at 9:51 pm

    Hi Kelvin. Great Job here!

    Do you have a guide on how to get these variables, or where do I get them from?

    $ApplicationId = ‘Application ID’
    $ApplicationSecret = ‘Application Secret’ | Convertto-SecureString -AsPlainText -Force
    $RefreshToken = ‘YourRefreshTokens’
    $ExchangeRefreshToken = ‘YourExchangeRefresherToken’
    $upn = ‘UPN-That-Generated-Tokens’

    Thanks

  2. Jminnebo November 28, 2019 at 10:39 am

    Hi,

    Having a little issue here;

    Documenting Customer
    Finding Customer in IT-Glue
    Uploading Office Permission Customer into IT-Glue
    Creating new Office Permission Customer into IT-Glue organisation ‘randomnumberhere’
    New-ITGlueFlexibleAssets : {“errors”:[{“source”:{“pointer”:”/data/attributes/flexible-asset-traits.tenantid”},”detail”:
    [“can’t be blank”],”title”:”Unprocessable Entity”,”status”:422}]}
    At C:\Office365MBpermissions.ps1:142 char:13
    + New-ITGlueFlexibleAssets -data $FlexAssetBody
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,New-ITGlueFlexibleAssets

    Exception calling “Add” with “2” argument(s): “Item is al toegevoegd. Sleutel in woordenboek: organization-id Sleutel die wordt toegevoegd: organization-id”
    At C:\Office365MBpermissions.ps1:139 char:13
    + $FlexAssetBody.attributes.add(‘organization-id’, $org)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentException

    Exception calling “Add” with “2” argument(s): “Item is al toegevoegd. Sleutel in woordenboek: flexible-asset-type-id Sleutel die wordt toegevoegd: flexible-asset-type-id”
    At C:\Office365MBpermissions.ps1:140 char:71
    + … exAssetBody.attributes.add(‘flexible-asset-type-id’, $($filterID.ID))
    + ~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentException

    Any clue where what I’m doing wrong : )?

    1. Kelvin Tegelaar November 28, 2019 at 11:07 am

      Fixed, Turns out the last update introduced a little error. Should not longer happen. have fun!

  3. Euan December 3, 2019 at 5:57 pm

    I’m getting weird results on the IT Glue script. Getting this for every account *Blanked out details.

    Error on proxy command ‘Get-MailboxPermission -Identity:’CN=******,OU=*********.onmicrosoft.com,OU=Microsoft Exchange Hosted
    Organizations,DC=GBRP265A002,DC=PROD,DC=OUTLOOK,DC=COM” to server CWLP123MB2865.GBRP123.PROD.OUTLOOK.COM: Server version 15.20.2495.0000, Proxy method PSWS:
    Cmdlet error with following error message:
    System.Management.Automation.ParentContainsErrorRecordException: The term ‘Get-MailboxPermission’ is not recognized as the name of a cmdlet, function, script file, or operable program.
    Check the spelling of the name, or if a path was included, verify that the path is correct and try again…
    + CategoryInfo : NotSpecified: (:) [Get-MailboxPermission], CmdletProxyException
    + FullyQualifiedErrorId : [Server=CWLP265MB0930,RequestId=cbc0bb2d-81ad-4174-8c1c-b16e67df2175,TimeStamp=03/12/2019 16:54:10] [FailureCategory=Cmdlet-CmdletProxyException] 1BE14FDB,Micr
    osoft.Exchange.Management.RecipientTasks.GetMailboxPermission
    + PSComputerName : ps.outlook.com

    It does sync to IT Glue however the tabling is wonky as the HTML tags are showing and some formatting is out.
    Also it’s just filling every Org in IT Glue with the first Office 365 tenant for each one.

    Any ideas?

    Thanks

    1. Kelvin Tegelaar December 3, 2019 at 6:58 pm

      Seems like my WordPress plugin is still messing up some code – I’ve fixed it.

      The get-mailbox permission is due to shared mailboxes not having all values avaialble. I’ll be solving that in a new version.

      1. Ken March 6, 2020 at 10:45 pm

        Any update on the error above being fixed.

        Thanks

        1. Kelvin Tegelaar March 8, 2020 at 2:52 pm

          Should be fixed already, are you experiencing the same? did you copy the latest version of the blog?

  4. Pingback: Documenting with PowerShell: Documenting Azure AD Settings - CyberDrain

  5. Mat March 14, 2020 at 1:45 pm

    Hello Can i use general HTML version in my Solarwinds RMM?

  6. Greg Clark August 20, 2020 at 5:10 pm

    Kelvin,

    I have a number of clients where the resulting HTML report only shows the DiscoverySearchMailbox and Discovery Manager as having permissions, even though I know that users have permissions to other users mailboxes in O365. What would be the cause?

    1. Kelvin Tegelaar August 20, 2020 at 5:46 pm

      I’ve been seeing this too, Greg. I’m working on a revised version that relies on the Graph API instead. Hopefully that’ll run better.

      Regards,

      1. Alp September 14, 2021 at 2:03 am

        Any Updates on how to get rid of DiscoverySearchMailbox?
        Additionally, I am seeing duplicate entries being created (IT Glue)

        With Following errors

        Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘organization-id’ Key being added: ‘organization-id'”
        At line:138 char:13
        + $FlexAssetBody.attributes.add(‘organization-id’, $org)
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : ArgumentException

        Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘flexible-asset-type-id’ Key being added: ‘flexible-asset-type-id'”
        At line:139 char:71
        + … exAssetBody.attributes.add(‘flexible-asset-type-id’, $($filterID.ID))
        + ~~~~~~~~~~~~
        + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : ArgumentException

  7. Pingback: Cyberdrain Automatic Documentation Scripts to Hudu – MSPP

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.