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" }
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
Hi Euan,
To get these, check my Secure Application Model blog at https://www.cyberdrain.com/connect-to-exchange-online-automated-when-mfa-is-enabled-using-the-secureapp-model/
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 : )?
Fixed, Turns out the last update introduced a little error. Should not longer happen. have fun!
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
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.
Any update on the error above being fixed.
Thanks
Should be fixed already, are you experiencing the same? did you copy the latest version of the blog?
Pingback: Documenting with PowerShell: Documenting Azure AD Settings - CyberDrain
Hello Can i use general HTML version in my Solarwinds RMM?
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?
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,