Entra / Microsoft 365 · Users & guests
Get Planner plans for user (device code)
Use Microsoft Graph with device code authentication to report the Planner plans a user can access.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
# Review required modules and connection steps before running.# Connect to Microsoft Graph or Exchange Online as needed for this script.
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "")$UserCredential = Get-Credential -Message "Enter credentials for the account you want to use to fetch Plan information"$User = $UserCredential.UserName# Application (client) ID, tenant ID, resource and scope$ClientID = "ded88173-911c-42a5-892b-26d7bea4c788" #GetPlansV2$Resource = "https://graph.microsoft.com/"$Scope = "Group.Read.All Group.ReadWrite.All User.Read User.Read.All"$CodeBody = @{resource = $resourceclient_id = $clientIdscope = $scope }# Get OAuth Code$CodeRequest = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$tenantId/oauth2/devicecode" -Body $CodeBody# Print Code to consoleWrite-Host "`n$($codeRequest.message)"$Body = @{grant_type = "urn:ietf:params:oauth:grant-type:device_code"code = $codeRequest.device_codeclient_id = $clientId}# Get OAuth Token$Token = $Null; $TokenRequest = $Nullwhile ([string]::IsNullOrEmpty($tokenRequest.access_token)) {$tokenRequest = try {Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$tenantId/oauth2/token" -Body $Body }catch {$errorMessage = $_.ErrorDetails.Message | ConvertFrom-Json# If not waiting for auth, throw errorif ($errorMessage.error -ne "authorization_pending") {throw}}}$Token = $tokenRequest.access_token$Headers = @{Authorization = "Bearer $Token"}# Find all Microsoft 365 Groups the user belongs to$Uri = "https://graph.microsoft.com/beta/users/$User/transitiveMemberOf"$MemberOf = Invoke-WebRequest -Headers $Headers -Uri $Uri | ConvertFrom-Json# Put the result in a list of groups we can process ;ater$GroupsMemberOf = [System.Collections.Generic.List[Object]]::new()ForEach ($M in $MemberOf.Value) {If ($M.GroupTypes -eq "Unified") { # Only select Microsoft 365 Groups$ReportLine = [PSCustomObject][Ordered]@{GroupId = $M.IdName = $M.DisplayName }$GroupsMemberOf.Add($ReportLine) }}# If there are any more groups to get, fetch them using the Nextlink given by the Graph and add them to the list$NextLink = $MemberOf.'@Odata.NextLink'While ($NextLink -ne $Null) {Write-Host "Still processing..."$MemberOf = Invoke-WebRequest -Method GET -Uri $NextLink -ContentType "application/json" -Headers $Headers | ConvertFrom-JSonForEach ($M in $MemberOf.Value) {If ($M.GroupTypes -eq "Unified") { # Only select Microsoft 365 Groups$ReportLine = [PSCustomObject][Ordered]@{GroupId = $M.IdName = $M.DisplayName }$GroupsMemberOf.Add($ReportLine) }}$NextLink = $MemberOf.'@Odata.NextLink'} #End WhileCLS# We now have a list of Microsoft 365 Groups that the user belongs to, so we can check# the groups to find out which have plans and report details of the plans we find.$Activity = "Checking Plans for " + $User$Report = [System.Collections.Generic.List[Object]]::new(); $PlanNumber = 0$i = 0; $GroupCount = $GroupsMemberOf.CountForEach ($Group in $GroupsMemberOf) {$i++$ProgressBar = "Processing group " + $Group.Name + " (" + $i + " of " + $GroupCount + ")"Write-Progress -Activity $Activity -Status $ProgressBar -PercentComplete ($i/$GroupCount*100)$PlanURI = 'https://graph.microsoft.com/V1.0/groups/' + $Group.GroupId + '/planner/plans'$Plans = Invoke-WebRequest -Method GET -Uri $PlanURI -ContentType "application/json" -Headers $Headers | ConvertFrom-JsonForEach ($Plan in $Plans.Value) {$PlanId = $Plan.Id$PlanNumber++$PlanCreated = Get-Date($Plan.CreatedDateTime) -format g$PlanOwner = $Plan.Owner # Microsoft 365 Group$PlanTitle = $Plan.Title$BucketURI = 'https://graph.microsoft.com/v1.0/planner/plans/' + $PlanId + '/buckets/'$Buckets = Invoke-RestMethod -Method GET -Uri $BucketURI -ContentType "application/json" -Headers $Headers$NumberBuckets = $Buckets.Value.Count$TasksURI = 'https://graph.microsoft.com/v1.0/planner/plans/' + $PlanId + '/tasks/'$Tasks = Invoke-RestMethod -Method GET -Uri $TasksURI -ContentType "application/json" -Headers $Headers$NumberTasks = $Tasks.'@odata.count'[DateTime]$LastTask = "1-Jan-1999"# Grab some data about tasks like the date of the latest task and task completion stats$TasksNotStarted = 0; $TasksInProgress = 0; $TasksComplete = 0ForEach ($Task in $Tasks.Value) {If (-not [string]::IsNullOrEmpty($Task.CreatedDateTime)) {[DateTime]$TaskCreated = Get-Date($Task.CreatedDateTime) -format g }If ($TaskCreated -gt $LastTask) {$LastTask = $TaskCreated; $LastTaskTitle = $Task.Title}Switch ($Task.PercentComplete) { #Generate stats for task completion status0 {$TasksNotStarted++}50 {$TasksInProgress++}100 {$TasksComplete++}} #End switch} # End ForIf ($LastTask -eq "1-Jan-1999") { # Check how long it's been since a task was created in the plan$LastTaskDate = "No tasks"; $DaysSinceTask = "N/A"}Else {$LastTaskDate = Get-Date($LastTask) -format g$DaysSinceTask = (New-TimeSpan($LastTask)).Days }# Write out information about plan$ReportLine = [PSCustomObject][Ordered]@{GroupId = $Group.GroupIdName = $Group.NamePlanId = $PlanIdTitle = $PlanTitleCreated = $PlanCreatedBuckets = $NumberBucketsTasks = $NumberTasksNotStarted = $TasksNotStartedInProgress = $TasksInProgressComplete = $TasksCompleteLastTaskDate = $LastTaskDateDaysSinceTask = $DaysSinceTaskLastTaskTitle = $LastTaskTitle }$Report.Add($ReportLine) } # End processing plan} # End GroupsWrite-Host "All done. " $PlanNumber "plans found in" $GroupCount "Microsoft 365 Groups for user" $User$Report | Select Name, Title, Created, Tasks, NotStarted, InProgress, Complete, LastTaskDate, DaysSinceTask, Buckets | Out-GridView
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.Attribution
Author
Office365itpros