Category Archives: Uncategorized

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!