I’ve previously posted a PowerShell script I put together to report the file and folder structure in OneDrive. This script used PowerShell and Graph API to loop through all files and folders and output the information to a CSV. I’ve recently had a requirement to add to that script, I didn’t just need the file structure, but also any details on any sharing that was in place.
This information is not easy to get as the reports in the Microsoft 365 Admin Center focus on recent activity rather than ‘as-is’ state. I’ve enhanced the earlier report and created a new script to add the additional details required. The script which is available on GitHub here can be used as a template for similar requirements.
To run the script, import the module as before and run as below:
getonedrivereport -ClientID <Application ID> -TenentID <AAD Directory ID> -ClientSecret <App registration Secret> -UserListCSV <a csv of users>
The particular paramaters required are:
- ClientID – This is the client ID of the application registration detailed below
- TenentID – This is the AAD Directory ID
- ClientSecret – This is a secret generated for the application registration
- UserListCSV – This is a CSV with the heading UserPrincipalName and the UPNs of each user you want to check
For the Application Registration, you will need the user.read.all and file.read.all permissions assigned.
The script contents are detailed below, as always, please don’t run online scripts directly in your production environment until you understand them and tailor them to your needs, this script is just an example.
##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 along with any sharing permissions 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
Param(
[parameter(Mandatory = $true)]
$clientId,
[parameter(Mandatory = $true)]
$tenantId,
[parameter(Mandatory = $true)]
$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
)
##If Shared, get the permissions
if ($item.shared) {
$Permissions = GetSharedFilePermissions -itemID $item.id -Token $token -itemname $item.name -Username $user.userprincipalname
write-host "found $($permissions.roles) permissions for $($file.name)" -ForegroundColor blue
}##Else blank out permission variable
else {
$permissions = $null
}
##If there are multiple, build multiple objects and export each
if ($Permissions -is [array]) {
foreach ($permission in $permissions) {
##Build file object
$object = [PSCustomObject]@{
User = $user.userprincipalname
ID = $item.id
FileName = $File.name
shared = $File.shared
LastModified = $File.lastModifiedDateTime
Filepath = $filepath
ItemID = $permission.itemID
ItemName = $permission.itemName
hasPassword = $permission.haspassword
roles = $permission.roles
DirectPermissions = $permission.DirectPermissions
LinkPermissions = $permission.LinkPermissions
}
##Export File Object
$datestamp = (get-date).tostring('yyMMdd')
$object | export-csv "OneDriveSharingReport-$datestamp.csv" -NoClobber -NoTypeInformation -Append
}
}
else {
##Build file object
$object = [PSCustomObject]@{
User = $user.userprincipalname
ID = $item.id
FileName = $File.name
shared = $File.shared
LastModified = $File.lastModifiedDateTime
Filepath = $filepath
ItemID = $permissions.itemID
ItemName = $permissions.itemName
hasPassword = $permissions.haspassword
roles = $permissions.roles
DirectPermissions = $permissions.DirectPermissions
LinkPermissions = $permissions.LinkPermissions
}
##Export File Object
$datestamp = (get-date).tostring('yyMMdd')
$object | export-csv "OneDriveSharingReport-$datestamp.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 GetSharedFilePermissions {
<#
.SYNOPSIS
Returns sharing details for input item
.PARAMETER itemID
-APIURi is the ID of the current item
.PARAMETER itemName
-token is the item to be processed
.PARAMETER token
-token is the auth token
.PARAMETER username
-token is current processed user
#>
Param(
[parameter(Mandatory = $true)]
[String]
$itemID,
[parameter(Mandatory = $true)]
[String]
$itemName,
[parameter(Mandatory = $true)]
[String]
$Token,
[parameter(Mandatory = $true)]
[String]
$Username
)
##Build Query
$apiuri = "https://graph.microsoft.com/beta/users/$username/drive/items/$itemID/permissions"
##Pass to run uery function
$Permissions = RunQueryandEnumerateResults -token $token -apiUri $apiuri
##Build an array to hold results
$Permismissionarray = @()
##Loop through Permissions and create object to hold results. If there are multiple these will be appended to the array
foreach ($permission in $permissions) {
$PermissionObject = New-Object PSObject -Property @{
ItemID = $itemID
ItemName = $itemName
hasPassword = $permission.haspassword
roles = $permission.roles[0]
DirectPermissions = $permission.grantedto.user.email -join (' ')
LinkPermissions = $permission.grantedtoidentities.user.email -join (' ')
}
$Permismissionarray += $PermissionObject
return $Permismissionarray
}
}
function getonedrivereport {
<#
.SYNOPSIS
Main function, reports on file and folder structure in OneDrive for all imported users
#>
Param(
[parameter(Mandatory = $true)]
$clientId,
[parameter(Mandatory = $true)]
$tenantId,
[parameter(Mandatory = $true)]
$clientSecret,
[parameter(Mandatory = $true)]
$UserListCSV
)
##Get in scope Users from CSV file##
$Users = import-csv $UserListCSV
#Loop Through Users
foreach ($User in $Users) {
#Generate Token
$token = GetGraphToken -clientID $clientId -TenantID $tenantId -clientSecret $clientSecret
##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
}
}
}
}
Very nice! Thanks for the great script. You’ve made a lot of things come together and look easy! I will likely use this for reference while I build a similar script for use with OneDrive (personal/business) and SharePoint Online document libraries. I wonder if you could share a few details:
– How come you used the Beta endpoint? I ask because I didn’t spot anything that I hadn’t seen in the v1.0 reference.
– Did you have a chance to test this for large sets of users/folders/files, and encounter any throttling?
I’ve thought about using the /$batch resource for JSON batching, but that does through a wrench into it when there are so many (overall/and) different requests to send. On the other hand, I could aim to handle the 429 responses’ (i.e. retry-after), but I’ve found when that is necessary, being able to handle those in batches is more efficient. The published service limits for these particular Graph resources are completely void of any detail (https://docs.microsoft.com/en-us/graph/throttling#files-and-lists-service-limits).
Thanks again.
LikeLike
Hey Jeremy,
Appreciate the comment, glad it can help you. The aim is not to provide solutions but explore and demonstrate possible solutions that can be built upon so glad to see it’s helping.
With regards to the Beta endpoint, that’s most likely just my habit of reusing old code, I don’t think anything here requires anything more than V1.0.
As for throttling, I’ve tested with about 2TB across 200 users and didn’t hit throttling. Batching could help, you could also try and catch the error and resubmit the function after a standard wait period. I’d be interested to see what solutions people come up with.
LikeLike