Automating with PowerShell: Teams Automapping

Something like 3 years ago I wrote a blog about using PowerShell to configure Onedrive sites using the odopen protocol. This was pretty much the only method to configure Onedrive to automatically map sites and have a zero-touch configuration.

Of course over the years the management side has improved and OneDrive usage has exploded. Unfortunately the onedrive automatic mapping structure isn’t where it should be yet. For example the GPO/intune method for automatic mapping configuration can take up to 8 hours to apply on any client.

During migrations and new deployment this is pretty much unacceptable. To make sure that mapping would be instant I’ve decided to create two scripts; One Azure Function which I’ve called AzMapper, and another client based script.

AzMapper requires you to create an Azure Function. To do that follow this manual and replace the script with the one below. This script is compatible with the Secure Application Model, and as such it can check all of your partner tenants too. Meaning you’ll only need to host a single version.

Replace $ApplicationID and $ApplicationSecret with your own from the Secure App Model.

You’ll also need to give your Secure Application Model a little more permissions, specifically to read the groups:

  • Go to the Azure Portal.
  • Click on Azure Active Directory, now click on “App Registrations”.
  • Find your Secure App Model application. You can search based on the ApplicationID.
  • Go to “API Permissions” and click Add a permission.
  • Choose “Microsoft Graph” and “Application permission”.
  • Search for “Sites” and click on “Sites.Read.All”. Click on add permission.
  • Search for “Team” and click on “TeamMember.Read.All”. Click on add permission.
  • Search for “Team” and click on “Team.ReadBasic.Alll”. Click on add permission.
  • Do the same for “Delegate Permissions”.
  • Finally, click on “Grant Admin Consent for Company Name.

AzMapper

using namespace System.Net
param($Request, $TriggerMetadata)
#
$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'ApplicationSecret'
#
$TenantID = $Request.Query.TenantID
$user = $Request.Query.Username

$body = @{
    'resource'      = 'https://graph.microsoft.com'
    'client_id'     = $ApplicationId
    'client_secret' = $ApplicationSecret
    'grant_type'    = "client_credentials"
    'scope'         = "openid"
}
$ClientToken = Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/token" -Body $body -ErrorAction Stop
$headers = @{ "Authorization" = "Bearer $($ClientToken.access_token)" }
$UserID = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/users/$($user)" -Headers $Headers -Method Get -ContentType "application/json").id

$AllTeamsURI = "https://graph.microsoft.com/beta/users/$($UserID)/JoinedTeams"
$Teams = (Invoke-RestMethod -Uri $AllTeamsURI -Headers $Headers -Method Get -ContentType "application/json").value
$MemberOf = foreach ($Team in $Teams) {
    $SiteRootUri = "https://graph.microsoft.com/beta/groups/$($Team.id)/sites/root"
    $SiteRootReq = Invoke-RestMethod -Uri $SiteRootUri  -Headers $Headers -Method Get -ContentType "application/json"
    $SiteDrivesUri = "https://graph.microsoft.com/beta/groups/$($Team.id)/sites/root/Lists"
    $SitesDrivesReq = (Invoke-RestMethod -Uri $SiteDrivesUri -Headers $Headers -Method Get -ContentType "application/json").value | where-object { $_.Name -eq "Shared Documents" }
    $DriveInfo = $SitesDrivesReq.ParentReference.siteid -split ','
    if($SiteRootReq.description -like "_no-auto-map_"){ continue }
if ($null -eq [System.Web.HttpUtility]::UrlEncode($SitesDrivesReq.id)) { continue }
[pscustomobject] @{
SiteID = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo[1])}")
WebID = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo[2])}")
ListID = [System.Web.HttpUtility]::UrlEncode($SitesDrivesReq.id)
        WebURL    = [System.Web.HttpUtility]::UrlEncode($SiteRootReq.webUrl)
Webtitle = [System.Web.HttpUtility]::UrlEncode($($Team.displayName)).Replace("+", "%20")
listtitle = [System.Web.HttpUtility]::UrlEncode($SitesDrivesReq.name)
}

}

# Associate values to output bindings by calling 'Push-OutputBinding'.

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = $MemberOf
})

If you want sites to be skipped, you can add “no-auto-map” in the description in Teams. this will cause the script to skip that site.

Now we can browse to the AzMapper URL to check if our function is working. To test the AzMapper click on “Get Function URL” in the Azure Portal and copy the URL required. You’ll end up with something like:

```
https://azMapper.azurewebsites.net/api/AzOneMap?code=verylongapicodehere==
````

an example of a test url would be:

```
https://azMapper.azurewebsites.net/api/AzOneMap?code=verylongapicodehere==&Tenantid=TENANTIDHERE&Username=USERNAMEHERE
```

This should return all sites for that specific user, it will contain exactly the information you need to create a odopen:// URL.

Client script

So you can schedule the client script using whatever method you prefer – as a startup script, using your RMM, or just a one-off during migrations. When a site is already configured to be synced it will skip this site. We do assume that OneDrive is already configured and just waiting to sync sites. 😉

#########################
$AutoMapURL = "https://azmapper.azurewebsites.net/api/AzOneMap"
$AutomapAPIKey = "TheAPIKeyFromTheAppAlsoKnownAsTheCode"
#########################

write-host "Grabbing OneDrive info from registry" -ForegroundColor Green
$TenantID = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").ConfiguredTenantId
$TenantDisplayName = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").Displayname
$username = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").userEmail
$CurrentlySynced = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1\Tenants\$($tenantdisplayname)" -ErrorAction SilentlyContinue)
Write-Host "Retrieving all possible teams."  -ForegroundColor Green
$ListOfTeams = Invoke-RestMethod -Method get -uri "$($AutoMapURL)?Tenantid=$($TenantID)&Username=$($Username)&code=$($AutomapAPIKey)" -UseBasicParsing
$Upn = [System.Web.HttpUtility]::Urldecode($username)
foreach ($Team in $ListOfTeams) {
    write-host "Checking if site is not already synced" -ForegroundColor Green
    $sitename = [System.Web.HttpUtility]::Urldecode($Team.Webtitle)
if ($CurrentlySynced.psobject.Properties.name -like "*$($sitename) -*") {
        write-host "Site $Sitename is already being synced. Skipping." -ForegroundColor Green
        continue
    }
    else {
        write-host "Mapping Team: $Sitename" -ForegroundColor Green
        Start-Process "odopen://sync/?siteId=$($team.SiteID)&webId=$($team.webid)&listId=$($team.ListID)&userEmail=$upn&webUrl=$($team.Weburl)&webtitle=$($team.Webtitle)"
start-sleep 5
}
}

And that’s it! running this script will map all the sites this specific user has access to, it won’t give weird pop-ups for users that do not have access and this should help you ease all Teams deployments by a lot. As always, Happy PowerShelling!

Recent Articles

The return of CyberDrain CTF

CyberDrain CTF returns! (and so do I!)

It’s been since september that I actually picked up a digital pen equivalent and wrote anything down. This was due to me being busy with life but also my side projects like CIPP. I’m trying to get back into the game of scripting and blogging about these scripts. There’s still so much to automate and so little time, right? ;)

Monitoring with PowerShell: Monitoring Acronis Backups

Intro

This is a monitoring script requested via Reddit, One of the reddit r/msp users wondered how they can monitor Acronis a little bit easier. I jumped on this because it happened pretty much at the same time that I was asked to speak at the Acronis CyberSummit so it kinda made sense to script this so I have something to demonstrate at my session there.

Monitoring with PowerShell: Monitoring VSS Snapshots

Intro

Wow! It’s been a while since I’ve blogged. I’ve just been so swamped with CIPP that I’ve just let the blogging go entirely. It’s a shame because I think out of all my hobbies it’s one I enjoy the most. It’s always nice helping others achieve their scripting target. I even got a couple of LinkedIn questions asking if I was done with blogging but I’m not. Writing always gives me some more piece of mind so I’ll try to catch up again. I know I’ve said that before but this time I’ll follow through. I’m sitting down right now and scheduling the release of 5 blogs in one go. No more whining and no more waiting.