Using Delegated Access Permissions in PowerShell to Manage all Microsoft 365 Services

I recently posted about how we can use Delegated Access Permissions via a partner relationship to connect to an Exchange Online organization through PowerShell. This is a fantastic piece of functionality for MSPs and CSPs to manage multiple tenancies securely without having managing a set of admin identities for all of their customers.

To expand on the previous post, I thought I would put together each of the PowerShell modules that support delegated admin permissions in one place and also highlight any that I feel are missing.

In this post I will go through the connection methods (where available) using DAP for each of the below modules:

  • ExchangeOnline
  • MSOnline
  • Azure AD
  • MicrosoftTeams
  • Skype for Business
  • SharePoint Online
  • Security & Compliance Center

Exchange Online Module (v2)

I’ve gone through this one recently in another post so full information is available there. In short, we cann connect to Exchange Online Powershell using the Exchange Online (v2) PowerShell Module by specifying the tenant domain in our connection command.

First, install the module as normal:

Install-Module ExchangeOnline

Once installed, restart PowerShell and connect using the customer tenancy domain:

Connect-ExchangeOnline -DelegatedOrganization <customerdomain.onmicrosoft.com>

MS Online Module

The MS Online Module works a little differently in that we don’t connect directly to our customer tenancy, we specify the tenancy in our commands.

We install the module with:

Install-Module MSOnline

Then we connect to our own service as normal:

Connect-MsolService

Once we are connected, we need to locate the Tenant ID of our target organization. If we don’t have it to hand we can find it using the tenant domain in the below command:

Get-MsolPartnerContract -DomainName <customerdomain.onmicrosoft.com> | Select-Object TenantID

Once we have the TenantID output (which will be a GUID), we can run commands against the tenant as below, using the -TenantID flag:

Get-MsolUser -All -TenantId <TenantID>

Azure AD Module

To connect to Azure AD, we need the Tenant ID from above to use in our connection. We can install the AzureADPreview Module:

Install-Module AzureADPreview

We then connect using our Tenant ID with the below command:

Connect-AzureAD -TenantId <TenantID>

Microsoft Teams Module

For Microsoft Teams we use the Tenant ID again. Install with:

Install-Module MicrosoftTeams

And then we connect with the Tenant ID as below:

Connect-MicrosoftTeams -TenantId <TenantID>

Skype for Business Module

The Skype for Business Module is interesting in that a lot of organizations have moved off Skype to use Microsoft Teams. The Skype module is still required to manage certain aspects of Teams though. The connection to the module is equally as strange. Once we have connected to Teams as above, we then new to create out connection to Skype using the below commands to create the session and then import it:

$session = New-CsOnlineSession
Import-PSSession $session

This will connect our existing Teams session to the Skype for Business module!

SharePoint Online Module

Unfortunately the SharePoint Online Module does not support DAP at the moment. I will update this post when/if it becomes available.

Security & Compliance Center Module

The Security and Compliance Center Module is installed as part of the Exchange Online (v2) module and allows connection to services such as DLP and Information Protection.

To connect to the Security & Compliance Center we can install the Exchange Online (v2) module as above and use the -DelegatedOrganization flag to specify our customer domain:

Connect-IPPSSession -DelegatedOrganization <CustomerDomain>

And that’s it, that’s pretty much all the modules I use on a daily basis, I will update this post as/when more updates or modules are available.

Coming Soon: Expiration For Guest User Access To SharePoint Online / OneDrive Documents

External file sharing is always a struggle to get right in Microsoft 365. Based on factors like company culture, industry and relationship to partner companies, there are a huge amount of variables that will influence the exact policy and controls that are required to tailor the relevant features.

I can’t stress enough how much setting this up right, as early as possible will save time in the long run. Providing a flexible, secure and scalable solution to external collaboration is a key factor to setting up any Office 365 tenant. Luckily, there are a myriad of features available in Office 365 to help provide the governance we need.

When we need to protect key specific information types we have Microsoft Information Protection and Data-Loss Prevention. When we need to protect Identity and Authorization, we have things like B2B, Identity Governance, Identity Protection and Conditional Access. The list goes on.

Each of these features (which mostly require licensing) are fantastic at meeting complex requirements but are not a replacement for a good baseline configuration of the toolset. One of the most important pieces of configuration we can get in place for our tenant with regards to file sharing and collaboration are the base sharing settings in the SharePoint Online Admin Center.

These settings control who we allow our users to share with, which users we allow to share externally and what controls apply to our guest users who are shared information.

A new addition to these controls is due in Jan 2021is the ability to set an admin level expiration on all OneDrive and SharePoint sharing links to Guests. This is a nice feature to have and will help to avoid perpetual access to externally shared data.

This setting will be disabled by default on roll-out so it is worth considering if it will fit into your configuration and planning communication to users.

Prevent Video Download in OneDrive and SharePoint Sharing Links

In line with the change to Stream videos storage to OneDrive and SharePoint. Microsoft are now enabling the ability to control the download functionality on shared videos. Previously, when sharing documents from OneDrive or SharePoint Online, users had the ability to make a file read-only and restrict download at the time of sharing.

Planned for late October for targeted release tenant and November for all other tenants, this feature should be available to everyone by the end of November. Once enabled, when sharing a video from OneDrive or SharePoint Online, users will have the ‘Block Download’ option. This option will ensure that recipients of the sharing link can only view the video in the web browser via the OneDrive web previewer.

Block download

While it’s not recommended for most use cases, this option can be disabled on a site or tenant level via the below commands in the SharePoint Online Management Shell respectively:

Tenant:

Set-SPOTenant -BlockDownloadLinksFileType:$True

Site:

Set-SPOSite -Identity <Site URL> -BlockDownloadLinksFileType:$True

Microsoft Teams Meeting Recordings Moving to OneDrive and SharePoint

Microsoft Teams recordings are a great feature for when people miss meetings and need to catch up, or when they just need to review the content. We’ve used them in the past for technical demos and project handovers. Recordings were were, until now, saved into Microsoft Stream and available to view in the Stream app. This is changing in the near future.

Going forward, Teams recordings will be saved into OneDrive and SharePoint by default instead. For any Channel meetings, recordings will be stored in the appropriate SharePoint library and for regular, non-channel meetings, they will be saved in the OneDrive of the user who hits the record button.

The current schedule for this rollout as issued by the Microsoft Message Center is:

  • mid-October (October 19, 2020) – You can enable the Teams Meeting policy to have meeting recordings saved to OneDrive and SharePoint instead of Microsoft Stream (Classic)
  • End of October (October 31, 2020) – Meeting recordings in OneDrive and SharePoint will have support for English captions via the Teams transcription feature.
  • Early to mid-November (Rolling out between November 1 -15 , 2020) – All new Teams meeting recordings will be saved to OneDrive and SharePoint unless you delay this change by modifying your organization’s Teams Meeting policies and explicitly setting them to “Stream”
  • Q1 2021 – No new meeting recordings can be saved to Microsoft Stream (Classic); all customers will automatically have meeting recordings saved to OneDrive and SharePoint even if they’ve changed their Teams meeting policies to Stream”

To delay this change until Q1 2020, you can run the below command in the Skype Online Management Shell (Yes it’s still around and very much needed to manage Teams, however it’s now installed as part of the Microsoft Teams PowerShell Module)

Set-CsTeamsMeetingPolicy -Identity Global -RecordingStorageMode "Stream"

The above command will allow you to defer this change to 2021 but not prevent it entirely.

For more information on the benefits of this change – and the limitations after moving, see the below Microsoft Article:

https://docs.microsoft.com/en-us/MicrosoftTeams/tmr-meeting-recording-change

Using Graph API in PowerShell Example – OneDrive File Structure Report

Due to an issue on a file migration, I recently had a requirement to compare source and destination OneDrive structures. The easiest way I could come up with to do this was to use Graph API to expand the folder structure and export to CSV. I’ve always been a big PowerShell users so that is usually the basis for my Graph scripts.

I decided to share this basic script to help anyone who is trying to figure out how it works. The source for this script can be found on GitHub here.

The below script is intended to illustrate how you can use PowerShell and Graph calls together, not as a production Script

##Author: Sean McAvinue
##Details: Used as a Graph/PowerShell example, 
##          NOT FOR PRODUCTION USE! USE AT YOUR OWN RISK
##          Returns a report of OneDrive file and folder structure to CSV file
function GetGraphToken {
    <#
    .SYNOPSIS
    Azure AD OAuth Application Token for Graph API
    Get OAuth token for a AAD Application (returned as $token)
    
    #>

    # Application (client) ID, tenant ID and secret
    $clientId = ""
    $tenantId = ""
    $clientSecret = ""
    
    
    # Construct URI
    $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
     
    # Construct Body
    $body = @{
        client_id     = $clientId
        scope         = "https://graph.microsoft.com/.default"
        client_secret = $clientSecret
        grant_type    = "client_credentials"
    }
     
    # Get OAuth 2.0 Token
    $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
     
    # Access Token
    $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
    
    #Returns token
    return $token
}
    

function expandfolders {
    <#
    .SYNOPSIS
    Expands folder structure and sends files to be written and folders to be expanded
  
    .PARAMETER folder
    -Folder is the folder being passed
    
    .PARAMETER FilePath
    -filepath is the current tracked path to the file
    
    .NOTES
    General notes
    #>
    Param(
        [parameter(Mandatory = $true)]
        $folder,
        [parameter(Mandatory = $true)]
        $FilePath

    )

    write-host retrieved $filePath -ForegroundColor green
    $filepath = ($filepath + '/' + $folder.name)
    write-host $filePath -ForegroundColor yellow
    $apiUri = ('https://graph.microsoft.com/beta/users/' + $user.UserPrincipalName + '/drive/root:' + $FilePath + ':/children')

    $Data = RunQueryandEnumerateResults -ApiUri $apiUri -Token $token

    ##Loop through Root folders
    foreach ($item in $data) {

        ##IF Folder
        if ($item.folder) {

            write-host $item.name is a folder, passing $filePath as path
            expandfolders   -folder $item -filepath $filepath

            
        }##ELSE NOT Folder
        else {

            write-host $item.name is a file
            writeTofile -file $item -filepath $filePath

        }

    }


}
   
function writeTofile {
    <#
    .SYNOPSIS
    Writes files and paths to export file

    
    .PARAMETER File
    -file is the file name found
    
    .PARAMETER FilePath
    -filepath is the current tracked path
    
    #>
    Param(
        [parameter(Mandatory = $true)]
        $File,
        [parameter(Mandatory = $true)]
        $FilePath

    )

    ##Build file object
    $object = [PSCustomObject]@{
        User         = $user.userprincipalname
        FileName     = $File.name
        LastModified = $File.lastModifiedDateTime
        Filepath     = $filepath
    }

    ##Export File Object
    $object | export-csv OneDriveReport.csv -NoClobber -NoTypeInformation -Append

    ##Reset workingfilepath



}

function RunQueryandEnumerateResults {
    <#
    .SYNOPSIS
    Runs Graph Query and if there are any additional pages, parses them and appends to a single variable
    
    .PARAMETER apiUri
    -APIURi is the apiUri to be passed
    
    .PARAMETER token
    -token is the auth token
    
    #>
    Param(
        [parameter(Mandatory = $true)]
        [String]
        $apiUri,
        [parameter(Mandatory = $true)]
        $token

    )

    #Run Graph Query
    $Results = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($Token)" } -Uri $apiUri -Method Get)
    #Output Results for debug checking
    #write-host $results

    #Begin populating results
    $ResultsValue = $Results.value

    #If there is a next page, query the next page until there are no more pages and append results to existing set
    if ($results."@odata.nextLink" -ne $null) {
        write-host enumerating pages -ForegroundColor yellow
        $NextPageUri = $results."@odata.nextLink"
        ##While there is a next page, query it and loop, append results
        While ($NextPageUri -ne $null) {
            $NextPageRequest = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($Token)" } -Uri $NextPageURI -Method Get)
            $NxtPageData = $NextPageRequest.Value
            $NextPageUri = $NextPageRequest."@odata.nextLink"
            $ResultsValue = $ResultsValue + $NxtPageData
        }
    }

    ##Return completed results
    return $ResultsValue

    
}


function main {
    <#
    .SYNOPSIS
    Main function, reports on file and folder structure in OneDrive for all imported users

    #>

    ##Get in scope Users from CSV file##
    $Users = import-csv userlist.csv


    #Loop Through Users
    foreach ($User in $Users) {
    
        #Generate Token
        $token = GetGraphToken

        ##Query Site to get Site ID
        $apiUri = 'https://graph.microsoft.com/v1.0/users/' + $User.userprincipalname + '/drive/root/children'
        $Data = RunQueryandEnumerateResults -ApiUri $apiUri -Token $token

        ##Loop through Root folders
        ForEach ($item in $data) {

            ##IF Folder, then expand folder
            if ($item.folder) {

                write-host $item.name is a folder
                $filepath = ""
                expandfolders -folder $item -filepath $filepath

                ##ELSE NOT Folder, then it's a file, sent to write output
            }
            else {

                write-host $item.name is a file
                $filepath = ""
                writeTofile -file $item -filepath $filepath

            }

        }


    
    }
    
    
    
}

Protecting Office 365 Groups and Microsoft Teams with Sensitivity Labels (Preview)

I often end up in one of two conversations around Microsoft Teams governance with customers, the “Users can manage them themselves so we don’t need to worry” group, and the “Nobody gets a Team unless we follow this 20 step approval process and our service desk needs to set them up and lock them down” group.

Both options have their merits, but also their pitfalls. If we let everyone create teams we end up with sprawl and have no idea where our data is stored (why are there six “HR Teams”, and which one contains the right data?). On the other hand, if we don’t let our users use Teams without jumping through hoops while saying the alphabet backwards, in Latin, then we are preventing people from using some of the most powerful collaboration features available to them.

We usually end up finding a good middle ground in these discussions that leverages automation and some of the cool Information Protection features of Microsoft 365. My opinion on Teams provisioning process has been the same as it was for SharePoint sites, “I don’t care if we have ten thousand Teams, as long as they are named and protected correctly, the number doesn’t matter”.

This opinion was idealistic in the early days of Microsoft Teams as the governance features just weren’t where I wanted them to be. In the past year, Microsoft have taken strides in the features available and I’m pretty happy (albeit still a few features I’d like) with what’s available. I might even do a follow up post where I can rant about my Teams security and governance opinions down the line.

One feature that has made my life much easier since it was made available (in Preview) is the ability of Sensitivity Labels to be applied to Office 365 Groups / Teams / Sites . This feature allows us to define Sensitivity Labels which, when applied to a Group, can control the privacy level of the Group, the level of functionality available to unmanaged devices, and even the external access configuration.

During some early Teams projects I had automated scripts which changed these group settings in Azure AD, SharePoint sharing settings on the site level at the time of provisioning. That was a nightmare as it had to be maintained and knowledge transferred to the incumbent IT Teams.

When this feature is enabled, we just need to specify the settings in our sensitivity label and it’s all taken care of for us.

The site and group settings tab

To enable this feature for your tenant now, connect to the Azure AD Preview PowerShell Module and run the below to update the Directory Setting and enable MIP Labelling in Office 365 Groups.

##Copy Current Settings to $Settings Variable
$Setting = Get-AzureADDirectorySetting -Id (Get-AzureADDirectorySetting | where -Property DisplayName -Value "Group.Unified" -EQ).id

##Change EnableMIPLables setting
$Setting["EnableMIPLabels"] = "True"

##Write back changes to directory
Set-AzureADDirectorySetting -Id $Setting.Id -DirectorySetting $Setting

Next, connect to the Security & Compliance PowerShell Module and run the below to start the synchronization process between MIP and AAD.

Execute-AzureAdLabelSync

After a little time to replicate, you will be able to see the above page when configuring a new sensitivity label and then apply them to Teams/Groups/Sites and once the labels are deployed (usually 24 hours after creation) you’ll be able to apply them at provisioning time!