wks_graphapi.psm1
<#
For long running workflow, manage internally the token query / renew #> $global:tokens = $null $global:tokensDate = $null <############################################# FUNCTIONS ##############################################> <# get-graphToken Retrieves application token for use with Graph API #> function get-graphToken { [CmdletBinding()] param( [Parameter( ParameterSetName="simple", Mandatory=$true, HelpMessage="configuration object")] $config, [Parameter( ParameterSetName="default", Mandatory=$true, HelpMessage="tenant name (ie. contoso.onmicrosoft.com)")] [string]$tenantName, [Parameter( ParameterSetName="default", Mandatory=$true, HelpMessage="App Registration ID")] [string]$appId, [Parameter( ParameterSetName="default", HelpMessage="Graph API Scope")] [string]$scope='https://graph.microsoft.com/.default', [Parameter(Mandatory=$true)] [string]$appSecret ) if ($PSCmdlet.ParameterSetName -eq 'simple') { $graphTenantName = $config.TenantName $graphAppId = $config.AppId $graphScope = $config.Scope } if ($PSCmdlet.ParameterSetName -eq 'default') { $graphTenantName = $TenantName $graphAppId = $AppId $graphScope = $Scope } $Url = "https://login.microsoftonline.com/$graphTenantName/oauth2/v2.0/token" # Add System.Web for urlencode Add-Type -AssemblyName System.Web # Create body $Body = @{ client_id = $graphAppId client_secret = $appSecret scope = $graphScope grant_type = 'client_credentials' } # Splat the parameters for Invoke-Restmethod for cleaner code $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' # Create string by joining bodylist with '&' Body = $Body Uri = $Url } Write-Verbose "Query Token" # Request the token! Invoke-RestMethod @PostSplat } <# use-graphApi Generic function for calling Graph API. Includes token querying and error handling #> function use-graphApi { [cmdletbinding()] param( [string]$uri, [string]$method = 'get', [switch]$all, [switch]$beta, [switch]$raw, [string]$contentType='application/json; charset=utf-8', $body = $null, $token ) $retryCount = 1 $graphUrl = "https://graph.microsoft.com/" $version = 'v1.0/' if ($beta) {$version = 'beta/'} $uri = $graphUrl + $version + $uri # Create header $Header = @{ Authorization = "$($token.token_type) $($token.access_token)" } write-debug ($header |ConvertTo-Json -Depth 5) do { try { $result = @() if ($body -ne $null) { if ($contentType -like "*application/json*") { $body = $body |convertto-json -Depth 10 } write-verbose $Uri write-verbose $body $temp = Invoke-RestMethod -Uri $Uri -Headers $Header -Method $method -Body $body -ContentType $contentType if ($raw) { $result += $temp} else { $result += $temp.value } } else { $temp = Invoke-RestMethod -Uri $Uri -Headers $Header -Method $method if ($raw) { $result += $temp} else { $result += $temp.value } if ($all) { while($temp."@odata.nextLink") { $temp = Invoke-RestMethod -Uri ([uri]($temp."@odata.nextLink")) -Headers $Header -Method $method -ContentType $contentType if ($raw) { $result += $temp} else {$result += $temp.value} } } } $retryCount = 0 $result } catch { if ($_.Exception.Response -eq $null) { Write-Error $_.Exception.Message # if ($retryCount -eq 0) {throw $_} else {Write-Warning "Retry #$retryCount"} $retryCount-- } else { $Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $Reader.BaseStream.Position = 0 $Reader.DiscardBufferedData() $ResponseBody = $Reader.ReadToEnd() if ($ResponseBody.StartsWith('{')) { $ResponseBody = $ResponseBody | ConvertFrom-Json } #$ResponseBody.error $msg = $ResponseBody.error.code + " => " + $ResponseBody.error.message if ($ResponseBody.error.code -like 'InvalidAuthenticationToken') { $msg = $msg + " Please run 'get-graphToken' first." } throw $msg } } } while ($retryCount -gt 0) } function use-graphApiBatchGet { [cmdletbinding()] param( [string[]]$uris, [switch]$beta, $token ) $graphUrl = "https://graph.microsoft.com/" $version = 'v1.0/' if ($beta) {$version = 'beta/'} $uri = $graphUrl + $version + '$batch' # Create header $Header = @{ Authorization = "$($token.token_type) $($token.access_token)" "Content-Type" = "application/json" } $thisHeader = @{ "Content-Type" = "application/json" } $thisRequest = @() for($i = 0; $i -lt @($uris).count; $i++) { $thisRequest += @{ "url" = $uris[$i] "method" = "GET" "id" = "$($i + 1)" } } try { $body = (@{"requests"= $thisRequest} |convertto-json -Depth 5) Write-Verbose $body $responses = (Invoke-RestMethod -Uri $Uri -Headers $Header -Method 'POST' -Body $body).responses $reparses = @($responses |% { @{next=$_.body."@odata.nextLink"; id=$_.id} |select -Unique |? {$_.next -ne $null}}) Write-Verbose "Reparse $($reparses.count) URL: $($reparses -join ', ')" foreach ($reparse in $reparses) { write-verbose ($reparse |ConvertTo-Json) while($reparse.next) { $temp = Invoke-RestMethod -Uri ([uri]($reparse.next)) -Headers $Header -Method 'GET' -ContentType "application/json" write-verbose ($temp |ConvertTo-Json -depth 4) $data = $responses|? {$_.id -eq $reparse.id} |select -first 1 if ($data) { $data.body.value += $temp.value } else { Write-Error "not found response with id $($reparse.id)" } $reparse = @{next=$temp."@odata.nextLink"; id=$reparse.id} } } $responses } catch { if ($_.Exception.Response -eq $null) { throw $_.Exception.Message } else { $Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $Reader.BaseStream.Position = 0 $Reader.DiscardBufferedData() $ResponseBody = $Reader.ReadToEnd() if ($ResponseBody.StartsWith('{')) { $ResponseBody = $ResponseBody | ConvertFrom-Json } #$ResponseBody.error $msg = $ResponseBody.error.code + " => " + $ResponseBody.error.message if ($ResponseBody.error.code -like 'InvalidAuthenticationToken') { $msg = $msg + " Please run 'get-graphToken' first." } throw $msg } } } <# get-graphUsers Retrieve all tenant users #> function get-graphUsers { param( [string[]]$properties=$null, [string]$usertype='', # member or guest $token ) $Uri = 'users' $params = @() if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $params += '$select=' + ($properties -join ',') } if ($usertype -ne '') { $params += '$filter=userType eq ''' + $userType + '''' } if ($params.count -gt 0) { $uri = $uri + '?' + ($params -join '&') } #write-host $uri use-graphApi -uri $uri -method 'get' -token $token -all } <# get-graphSharedItemPermissions Retrieve a simplified view of an item's share permissions #> function get-graphSharedItemPermissions { param( [string]$userId, [string]$itemId, [string[]]$internalDomains, $token ) $Uri = "drives/$userId/items/$itemId/permissions" $result = @(use-graphApi -uri $uri -method 'get' -token $token -all) $response = @() foreach ($perm in $result) { if ($null -ne $perm.grantedToidentities) { foreach ($entity in $perm.grantedToidentities) { $response += New-Object -TypeName PSObject -Property @{ role = $perm.roles -join ',' scope = $perm.link.scope email = $entity.user.email name = $entity.user.displayName link = $perm.link.webUrl external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false} } } } elseif ($null -ne $perm.grantedTo) { foreach ($entity in $perm.grantedTo) { $response += New-Object -TypeName PSObject -Property @{ role = $perm.roles -join ',' scope = 'direct' email = $entity.user.email name = $entity.user.displayName link = '' external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false} } } } else { $response += New-Object -TypeName PSObject -Property @{ role = $perm.roles -join ',' scope = $perm.link.scope link = $perm.link.webUrl } } } #$result $response } function get-graphSharedItemPermissionsBatch { [CmdletBinding()] param( [string]$userId, [string[]]$itemIds, [string[]]$internalDomains, $token, $maxRetryCount = 5, [int]$sleep = 1 ) $retryList = $itemIds $responses=@{ throttlingDuration = 0; errors = @(); data = @{}; processingDuration = 0} $toProcess = @() $throttlingDelay = 0 $loopCount = 0 $processingStart = get-date do { $Uris = @($retryList |% { "drives/$userId/items/$_/permissions"}) Write-verbose "get-graphSharedItemPermissionsBatch: $($retryList -join ', ')" #write-verbose ($result |ConvertTo-Json -depth 5) $result = @(use-graphApiBatchGet -uri $uris -token $token -beta) $toProcess += $result |? { $_.status -eq 200 } # Comppute throttlong delay as max of all requested delay $throttlingDelay = 0 $throttlingDelay = ($result |? { $_.status -eq 429 } |% { $_.headers."Retry-After" } |measure -Maximum).maximum # Show errors $result |? { ($_.status -ge 400) -and ($_.status -ne 429) } |% { Write-error "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])" $responses.errors += "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])" } $retryList = @($result |? { $_.status -eq 429 } |% { $retryList[$_.id] }) |? { ($null -ne $_) -and ('' -ne $_)} if ($retryList.count -gt 0) { Write-Warning "Azure Graph API applied $throttlingDelay sec. throttling to $($retryList -join ', ')" $responses.throttlingDuration += new-timespan -seconds $throttlingDelay } if ($throttlingDelay -gt 0) { Start-Sleep -Seconds $throttlingDelay } else { Start-Sleep -Seconds $sleep } $loopCount++ } while (($retryList.count -gt 0) -and ($loopCount -lt $maxRetryCount)) if ($loopCount -eq $maxRetryCount) { Write-Warning "get-graphSharedItemPermissionsBatch maxRetryCount ($maxRetryCount) reached with $($retryList.count) items ignored" } foreach ($data in $toProcess) { #write-debug ($data |convertto-json -Depth 10) $itemId = $itemIds[($data.id - 1)] $responses.data[$itemId] = @() # Build list of permIds to prevent duplicates $permIds = @() Write-verbose "- Item $itemId -> Permissions count: $($data.body.value.count)" foreach ($perm in $data.body.value) { # New permission to process for this item if ($permIds -notcontains $perm.id) { $permIds += $perm.id if ($null -ne $perm.grantedToidentities) { foreach ($entity in $perm.grantedToidentities) { $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{ role = $perm.roles -join ',' scope = if ($perm.link.scope) {$perm.link.scope} else {''} id = $perm.id email = $entity.user.email name = $entity.user.displayName link = $perm.link.webUrl inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false} external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false} } } } elseif ($null -ne $perm.grantedTo) { foreach ($entity in $perm.grantedTo) { $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{ id = $perm.id role = $perm.roles -join ',' scope = 'direct' email = $entity.user.email name = $entity.user.displayName link = '' inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false} external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false} } } } else { $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{ id = $perm.id role = $perm.roles -join ',' scope = if ($perm.link.scope) {$perm.link.scope} else {''} link = $perm.link.webUrl inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false} } } } } if ($responses.data[$itemId].count -eq 0) { Write-Debug "Failed retrieving permission for item $itemId : $($data.body.error.code) -> $($data.body.error.message)" } } $responses.processingDuration = (get-date) - $processingStart $responses } <# get-graphSharedFiles Recursively parse an users's for shared files and returns a search result #> function get-graphSharedFiles { [CmdletBinding(DefaultParameterSetName = 'token')] param ( [Parameter(Mandatory = $true,ParameterSetName = 'token')] [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string]$itemId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains, [Parameter(Mandatory = $true, ParameterSetName = 'token', HelpMessage = 'Graph API token object')]$token, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'Graph API configuration')]$config, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'App Registration Secret')]$secret ) $internalToken = $token # Get / Refresh Token if parameterSet Autonomous (for long run) if (($null -ne $config) -and ($null -ne $secret)) { if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) { Write-Verbose "Querying autonomous token for get-graphSharedFiles" $internalToken = get-graphToken -config $config -appSecret $secret $global:tokens = $internalToken $global:tokensDate = get-date } else { $internalToken = $global:tokens } } $item = "root" $sharedResult = New-Object -TypeName PSObject -Property @{ userid=$userId sharedItemsCount=0 totalItems=0 startedAt=(get-date) endedAt=$null duration=$null sharedItems=@() } if ('' -ne $itemId) { $item = "items/$itemId"} $Uri = "drives/$userId/$item/children" + '?$top=10000&$inlinecount=allpages&$select=id,name,path,file,folder,shared,parentreference,weburl' Write-Verbose $uri $result = use-graphApi -uri $uri -method 'get' -token $internalToken -all foreach ($i in $result) { $sharedResult.totalItems++ $itemType = 'file' if ($null -ne $i.folder) { $itemType='folder'} #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))" if ($i.shared.scope.count -gt 0) { # The item is shared. Build metadata $sharedResult.sharedItemsCount++ $newItem = New-Object -TypeName PSObject -Property @{ userid=$userId itemid=$i.id name=$i.name scope=$i.shared.scope path=($i.parentreference.path -split '/root:')[1] type=$itemType sharedWith = get-graphSharedItemPermissions -userId $userId -itemId $i.id -internalDomains $internalDomains -token $internalToken } #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) } #Write-Host "-> SHARED" $sharedResult.sharedItems += $newItem } else { # Parse only non-shared sub-folders } # Parse all sub-folders if ($null -ne $i.folder) { Write-Verbose "Parsing subfolder : $($i.name)" $temp = @(get-graphSharedFiles -userid $userId -itemid $i.id -internalDomains $internalDomains -token $internalToken) if ($temp.sharedItemsCount -gt 0) { $sharedResult.sharedItems += $temp.sharedItems $sharedResult.totalItems += $temp.totalItems $sharedResult.sharedItemsCount += $temp.sharedItemsCount } $temp = $null } } $sharedResult.endedAt=(get-date) $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt) $sharedResult } function get-graphReportOneDriveAccountDetail { [CmdletBinding()] param( $token ) $Uri = 'reports/getOneDriveUsageAccountDetail(period=''D7'')?$format=application/json' use-graphApi -uri $uri -method 'get' -token $token -all -beta } function get-graphSharedFilesBatch { [CmdletBinding(DefaultParameterSetName = 'token')] param ( [Parameter(Mandatory = $true,ParameterSetName = 'token')] [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string[]]$itemId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains, [Parameter(Mandatory = $true, ParameterSetName = 'token', HelpMessage = 'Graph API token object')]$token, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'Graph API configuration')]$config, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'App Registration Secret')]$secret, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][switch]$beta ) $internalToken = $token # Get / Refresh Token if parameterSet Autonomous (for long run) if (($null -ne $config) -and ($null -ne $secret)) { if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) { Write-Verbose "Querying autonomous token for get-graphSharedFiles" $internalToken = get-graphToken -config $config -appSecret $secret $global:tokens = $internalToken $global:tokensDate = get-date } else { $internalToken = $global:tokens } } #$item = "root" $sharedResult = New-Object -TypeName PSObject -Property @{ userid=$userId sharedItemsCount=0 totalItems=0 startedAt=(get-date) endedAt=$null duration=$null sharedItems=@() } $uri =@() if ($itemId.count -ne 0) { $Uri = $itemId |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}} else {$Uri += ("drives/$userId/root/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl')} Write-Verbose ($uri -join ', ') $resultSet = use-graphApiBatchGet -uri $uri -token $internalToken -beta:$beta #write-host "- Processing $(@($resultSet).count) results" $subfolderIds = @() $result = @($resultset |% {$_.body.value}) #write-host "- Processing $($result.count) items" # Process files foreach ($i in @($result |? {$_.file -ne $null})) { $sharedResult.totalItems++ $itemType = 'file' # prepare object $newItem = New-Object -TypeName PSObject -Property @{ userid=$userId itemid=$i.id name=$i.name scope=$i.shared.scope path=($i.parentreference.path -split '/root:')[1] type=$itemType sharedWith = '' } #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))" if ($i.shared.scope.count -gt 0) { # The item is shared. Build metadata $sharedResult.sharedItemsCount++ #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) } #Write-Host "-> SHARED" $sharedResult.sharedItems += $newItem } } # process folders in batch mode foreach ($i in @($result |? {$_.folder -ne $null})) { $sharedResult.totalItems++ $itemType = 'folder' $subfolderIds += $i.id # prepare object $newItem = New-Object -TypeName PSObject -Property @{ userid=$userId itemid=$i.id name=$i.name scope=$i.shared.scope path=($i.parentreference.path -split '/root:')[1] type=$itemType sharedWith = '' } #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))" if ($i.shared.scope.count -gt 0) { # The item is shared. Build metadata $sharedResult.sharedItemsCount++ #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) } #Write-Host "-> SHARED" $sharedResult.sharedItems += $newItem } } # Parse all parsed subfolders at once in batch mode if ($subfolderIds.count -gt 0) { # Batch process subfolderparsing $parallelismLevel = 20 $iterations = [math]::Floor($subfolderIds.count / $parallelismLevel) Write-verbose "- $($subfolderIds.count) subfolder to parse" Write-verbose "Folder iterations: $iterations" for ($iteration=0; $iteration -le $iterations; $iteration++) { $count = $parallelismLevel if ($iteration -eq $iterations) { $count = $subfolderIds.count % $parallelismLevel } Write-verbose "Folder iteration loop: $iteration" Write-verbose "Folder iteration count: $count" $items = $subfolderIds |select -skip ($iteration * $parallelismLevel) -first $count #Write-Host "Parsing folder for $($items.count) items: $($items -join ', ')" $temp = get-graphSharedFilesBatch -userid $userId -itemid $items -internalDomains $internalDomains -token $internalToken -beta:$beta #write-verbose ($responses |convertto-json -Depth 5) #foreach ($i in $temp) { $sharedResult.totalItems += $temp.totalItems if ($temp.sharedItemsCount -gt 0) { $sharedResult.sharedItems += $temp.sharedItems $sharedResult.sharedItemsCount += $temp.sharedItemsCount } #} $temp = $null } } $sharedResult.endedAt=(get-date) $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt) $sharedResult } # Non-recursive version function get-graphSharedFilesBatch2 { [CmdletBinding(DefaultParameterSetName = 'token')] param ( [Parameter(Mandatory = $true,ParameterSetName = 'token')] [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string[]]$itemId, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains, [Parameter(Mandatory = $true, ParameterSetName = 'token', HelpMessage = 'Graph API token object')]$token, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'Graph API configuration')]$config, [Parameter(Mandatory = $true, ParameterSetName = 'autonomous', HelpMessage = 'App Registration Secret')]$secret, [Parameter(ParameterSetName = 'token')] [Parameter(ParameterSetName = 'autonomous')][switch]$beta ) $internalToken = $token # Get / Refresh Token if parameterSet Autonomous (for long run) if (($null -ne $config) -and ($null -ne $secret)) { if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) { Write-Verbose "Querying autonomous token for get-graphSharedFiles" $internalToken = get-graphToken -config $config -appSecret $secret $global:tokens = $internalToken $global:tokensDate = get-date } else { $internalToken = $global:tokens } } #$item = "root" $sharedResult = New-Object -TypeName PSObject -Property @{ userid=$userId sharedItemsCount=0 totalItems=0 totalFolders=0 startedAt=(get-date) endedAt=$null duration=$null sharedItems=@() errors=@() throttlingDuration = 0 } # count current depth folder level $depth = 0 # Build URI $subfolderIds = @() # Process a folder depth at once and go deeper do { Write-host "- Processing depth level $depth" $uri =@() # in first loop use function's input parameters if provided $result = @() if ($depth -eq 0) { # Build URI list for Graph API batch processing if ($itemId.count -ne 0) { $Uri = $itemId |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}} # If no itemid provided start at root level else {$Uri += ("drives/$userId/root/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl')} Write-Verbose ($uri -join ', ') $resultset = use-graphApiBatchGet -uri $uri -token $internalToken -beta:$beta $result += @($resultset |% {$_.body.value}) $errors = @($resultset |? { $_.status -ne 200 }) $sharedResult.errors+=$errors } else { # Batch process subfolderparsing $parallelismLevel = 20 $iterations = [math]::Floor($subfolderIds.count / $parallelismLevel) Write-verbose "- $($subfolderIds.count) subfolder to parse" Write-verbose "Folder iterations: $iterations" for ($iteration=0; $iteration -le $iterations; $iteration++) { $count = $parallelismLevel if ($iteration -eq $iterations) { $count = $subfolderIds.count % $parallelismLevel } Write-verbose "- Iteration loop: $iteration / $iterations" Write-verbose "Folder iteration count: $count" $items = @($subfolderIds |select -skip ($iteration * $parallelismLevel) -first $count) if ($items.count -gt 0) { # Use next loop iteration to parse subfolders $Uri = $items |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'} $retryList = $items $throttlingDelay = 0 $loopCount = 0 do { $Uris = @($retryList |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}) Write-verbose "get-graphSharedFilesBatch2: $($Uris -join ', ')" #write-verbose ($result |ConvertTo-Json -depth 5) $resultSet = @(use-graphApiBatchGet -uri $uris -token $internalToken -beta:$beta) #Collect results to process $result += @($resultSet |? { $_.status -eq 200 } |% {$_.body.value}) # Comppute throttlong delay as max of all requested delay $throttlingDelay = 0 $throttlingDelay = ($resultSet |? { $_.status -eq 429 } |% { $_.headers."Retry-After" } |measure -Maximum).maximum # Show errors $resultSet |? { ($_.status -ge 400) -and ($_.status -ne 429) } |% { $sharedResult.errors+= "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])" Write-error "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])" } # build retry list $retryList = @($resultSet |? { $_.status -eq 429 } |% { $retryList[$_.id] }) if ($retryList.count -gt 0) { Write-Warning "Azure Graph API applied $throttlingDelay sec. throttling to $($retryList -join ', ')" } if ($throttlingDelay -gt 0) { $sharedResult.throttlingDuration += $throttlingDelay Start-Sleep -Seconds ($throttlingDelay) } $loopCount++ } while (($retryList.count -gt 0) -and ($loopCount -lt $maxRetryCount)) if ($loopCount -eq $maxRetryCount) { Write-Warning "get-graphSharedItemPermissionsBatch maxRetryCount ($maxRetryCount) reached" if ($retryList.count -gt 0) { Write-error "$($retryList.count) childitem items ignored: $($retryList -join ', ')" } } } # Wait 1 sec every 2 queries if (($iteration / 2) -eq 1) { Start-Sleep -seconds 1} } # clear list for further processing $subfolderIds = @() } # Process results write-verbose "- Processing $($result.count) items" # Process files foreach ($i in @($result |? {$_.file -ne $null})) { $sharedResult.totalItems++ $itemType = 'file' # prepare object $newItem = New-Object -TypeName PSObject -Property @{ userid=$userId itemid=$i.id name=$i.name scope=$i.shared.scope path=($i.parentreference.path -split '/root:')[1] parentid=$i.parentreference.id type=$itemType sharedWith = @() } #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))" if ($i.shared.scope.count -gt 0) { # The item is shared. Build metadata $sharedResult.sharedItemsCount++ #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) } #Write-Host "-> SHARED" $sharedResult.sharedItems += $newItem } } # process folders in batch mode foreach ($j in @($result |? {$_.folder -ne $null})) { $sharedResult.totalItems++ $sharedResult.totalFolders++ $itemType = 'folder' $subfolderIds += $j.id # prepare object $newItem = New-Object -TypeName PSObject -Property @{ userid=$userId itemid=$j.id name=$j.name scope=$j.shared.scope path=($j.parentreference.path -split '/root:')[1] parentid=$j.parentreference.id type=$itemType sharedWith = @() } #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))" if ($j.shared.scope.count -gt 0) { # The item is shared. Build metadata $sharedResult.sharedItemsCount++ #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) } #Write-Host "-> SHARED" $sharedResult.sharedItems += $newItem } } # increase depth level $depth++ Write-verbose "- Finished processing depth level $depth" Write-host "- Need to process $($subfolderIds.count) subfolder at next depth level" } while ($subfolderIds.count -gt 0) $sharedResult.endedAt=(get-date) $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt) $sharedResult } function get-graphSharedFilesPermissionsBatch { [CmdletBinding()] param( $userId, $sharedItems, $internalDomains, $token ) Write-host "Processing Permissions" # Retrive permissions of retrieved shared items if ($sharedItems.sharedItemsCount -gt 0) { $parallelismLevel = 20 $iterations = [math]::Floor($sharedItems.sharedItemsCount / $parallelismLevel) Write-host "Permission total iterations: $iterations" for ($iteration=0; $iteration -le $iterations; $iteration++) { $count = $parallelismLevel if ($iteration -eq $iterations) { $count = $sharedItems.sharedItemsCount % $parallelismLevel } Write-Verbose "- Permission iteration loop: $iteration / $iterations" if ($count -gt 0) { Write-Debug "Permission iteration count: $count" $items = $sharedItems.sharedItems |select -skip ($iteration * $parallelismLevel) -first $count Write-Debug "Requesting permissions for $($items.count) items: $(($items |select -ExpandProperty itemid) -join ', ')" $responses = get-graphSharedItemPermissionsBatch -userId $userId -itemIds ($items |select -ExpandProperty itemid) -internalDomains $internalDomains -token $token # Put permission back to item list #write-verbose ($responses |convertto-json -Depth 5) foreach ($i in $items) { $i.sharedWith += $responses.data[$i.itemid] if ($i.sharedWith -eq 0) { #write-error "Permissions empty for $($i.name) ($($i.itemid))" } } # Increment feedback (counters, timers, errors) $sharedItems.errors += $responses.errors $sharedItems.duration += $responses.processingDuration $sharedItems.throttlingDuration += $responses.throttlingDuration if (($iteration % 100) -eq 99) { $delay = 30 if ($iteration -gt 200) { $delay = 60} Write-warning "Preventive wait for $delay sec." Start-Sleep -seconds $delay } } } } $sharedItems.endedAt=(get-date) $sharedItems } <# #> function send-graphMailReport { param( [Parameter(Mandatory=$true)][string]$senderId, [Parameter(Mandatory=$true)][string[]]$recipientAddress, [Parameter(Mandatory=$true)]$subject, [Parameter(Mandatory=$true)]$body, $htmlAttachment, [Parameter(Mandatory=$true)]$token ) $message = @{ "message"= @{ "subject"= "$subject" "body"= @{ "contentType"= "html" "content"= "$body" } "toRecipients"= @() } } $recipientAddress |% { $message.message.toRecipients += @{ "emailAddress" = @{ "address" = "$_" } } } if (($htmlAttachment -ne '') -and ($null -ne $htmlAttachment)) { $message.message.Add('attachments', @( @{ "@odata.type"= "#microsoft.graph.fileAttachment" "name"= "report.html" "contentType"= "text/html" "contentBytes"= "$([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($htmlAttachment)))" } )) } $uri ="users/$senderId/sendMail" #$message |convertto-json -depth 5 use-graphApi -uri $uri -method "POST" -body $message -token $token -raw -contentType "application/json; charset=utf-8" } function get-graphDomains { param( $token ) $Uri = 'domains' use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphGroups { param( [string[]]$properties=$null, $token ) $Uri = 'groups' if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphApplications { param( [string[]]$properties=$null, $token ) $Uri = 'applications' if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphApplication { param( [string]$id, [string[]]$properties=$null, $token ) $Uri = "applications/$id" if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphServicePrincipals { param( [string]$id, [string[]]$properties=$null, $token ) $Uri = "servicePrincipals" if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$top=999&$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphServicePrincipal { param( [string]$id, [string[]]$properties=$null, $token ) $Uri = "servicePrincipals/$id" if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphAadTermsAndConditions { param( [string]$id, [string[]]$properties=$null, $token ) $Uri = "termsAndConditions" if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphDirectoryRoles { param( [string[]]$properties=$null, $token ) $Uri = 'directoryRoles' if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } function get-graphDirectoryRoleTemplates { param( [string[]]$properties=$null, $token ) $Uri = 'directoryRoleTemplates' if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } <# CONDITIONNAL ACCESS #> function get-graphCAPolicies { param( [string[]]$properties=$null, $token ) $Uri = 'identity/conditionalAccess/policies' if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all -beta } function get-graphDirectoryObject { param( [string]$id, [string[]]$properties=$null, $token ) $Uri = "directoryObjects/$id" if ($null -ne $properties) { $properties |% { $_ = $_.trim() } $uri += '?$select=' + ($properties -join ',') } use-graphApi -uri $uri -method 'get' -token $token -all } <# Access Token routines #> <# .SYNOPSIS Does a Multi-Factor authentication against Azure using UserName and Password and a One Time Password OTP as the second factor .DESCRIPTION Does a Multi-Factor authentication against Azure using UserName and Password and a One Time Password OTP as the second factor .INPUTS PSCrednetails which will contain the username and password for the Primary Auth OTP code can be generated with the Get-TimeBasedOneTimePassword function (requires that this be setup beforehand) .OUTPUTS AccessToken .EXAMPLE PS C:\> Get-AccessTokenMFA -OTP 123456 Example use a SharedSecret stored in the Windows Credential Store PC C:\> Get-AccessTokenMFA -OTP (Get-TimeBasedOneTimePassword -SharedSecret (Get-StoredCredential -Target StoredAuth -AsCredentialObject).Password) .NOTES Author : Glen Scales .LINK #> function Get-AccessTokenMFA{ [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [PSCredential] $Credential, [Parameter(Position = 1, Mandatory = $true)] [String] $OTP, [Parameter(Position = 2, Mandatory = $false)] [String] $ClientId = "95a1b05c-60f1-420d-a5b2-0cca170dfadc", [Parameter(Position = 3, Mandatory = $false)] [String] $RedirectURI = "https://login.microsoftonline.com/common/oauth2/nativeclient", [Parameter(Position = 4, Mandatory = $false)] [String] $scopes = "https://outlook.office.com/EWS.AccessAsUser.All" ) process { $domain = $Credential.UserName.Split('@')[1] $openidURL = "https://login.windows.net/$domain/v2.0/.well-known/openid-configuration" $TenantId = (Invoke-WebRequest -Uri $openidURL -UseBasicParsing | ConvertFrom-Json).token_endpoint.Split('/')[3] $AuthURL = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=$ClientId&response_mode=form_post&response_type=code&redirect_uri=" + [System.Web.HttpUtility]::UrlEncode($RedirectURI) $StartLogon = Invoke-WebRequest -uri $AuthURL -UseBasicParsing -SessionVariable 'AuthSession' $Context = [regex]::Match($StartLogon.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value $Flow = [regex]::Match($StartLogon.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value $Canary = [regex]::Match($StartLogon.RawContent,"`"canary`":`"(.*?)`"").Groups[1].Value $FBAAuthBody=@{ "login" = $Credential.UserName "loginFmt" = $Credential.UserName "i13"="0" "type"="11" "LoginOptions"="3" "passwd"= $Credential.GetNetworkCredential().password.ToString() "ps"="2" "flowToken"=$Flow "canary"=$Canary "ctx"=$Context "NewUser"="1" "fspost"="0" "i21"="0" "CookieDisclosure"="1" "IsFidoSupported"="1" "hpgrequestid"=(New-Guid).ToString() } if (($Context -eq '') -or ($flow -eq '')) { throw "Authorize: $strServiceExceptionMessage. $($StartLogon.RawContent)" } $FBAResponse = Invoke-WebRequest -Uri "https://login.microsoftonline.com/common/login" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FBAAuthBody -WebSession $AuthSession -UseBasicParsing $Context = [regex]::Match($FBAResponse.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value $Flow = [regex]::Match($FBAResponse.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value $strServiceExceptionMessage = [regex]::Match($FBAResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value $SasBegin=@{ "AuthMethodId" = "PhoneAppOTP" "flowToken"=$Flow "ctx"=$Context "Method"="BeginAuth" } if (($Context -eq '') -or ($flow -eq '')) { throw "Login: $strServiceExceptionMessage. $($FBAResponse.RawContent)" } $SASBeginResponse = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/SAS/BeginAuth" -Method Post -ContentType "application/json" -Body ($SasBegin | ConvertTo-Json) -WebSession $AuthSession) $strServiceExceptionMessage = [regex]::Match($SASBeginResponse.RawContent,"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value if ($strServiceExceptionMessage -ne '') { throw "BeginAuth: $strServiceExceptionMessage" } $SASEnd=@{ "AdditionalAuthData"=$OTP "AuthMethodId"="PhoneAppOTP" "flowToken"=$SASBeginResponse.flowToken "ctx"=$SASBeginResponse.ctx "Method"="EndAuth" "PollCount"=1 "SessionId"=$SASBeginResponse.SessionId } if (($SASBeginResponse.flowToken -eq $null) -or ($SASBeginResponse.ctx -eq $null) -or ($SASBeginResponse.SessionId -eq '')) { throw "BeginAuth: $strServiceExceptionMessage. $($SASBeginResponse.RawContent)" } $SASEndResponse = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/SAS/EndAuth" -Method Post -ContentType "application/json" -Body ($SASEnd | ConvertTo-Json) -WebSession $AuthSession) $strServiceExceptionMessage = [regex]::Match($SASEndResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value if ($strServiceExceptionMessage -ne '') { throw "EndAuth: $strServiceExceptionMessage" } $SASProcess=@{ "type"=19 "GeneralVerify"="false" "otc"=$OTP "login"= $Credential.UserName "mfaAuthMethod"="PhoneAppOTP" "flowToken"=$SASEndResponse.flowToken "request"=$SASEndResponse.ctx "Method"="EndAuth" "PollCount"=1 "SessionId"=$SASEndResponse.SessionId "canary"=$Canary "hpgrequestid"=(New-Guid).ToString() } if (($SASEndResponse.flowToken -eq '') -or ($SASEndResponse.ctx -eq '') -or ($SASEndResponse.SessionId -eq '')) { throw "EndAuth: $strServiceExceptionMessage. $($SASEndResponse.RawContent)" } $SASProcessResponse = (Invoke-WebRequest -Uri "https://login.microsoftonline.com/common/SAS/ProcessAuth" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $SASProcess -WebSession $AuthSession -ea SilentlyContinue -UseBasicParsing) $strServiceExceptionMessage = [regex]::Match($SASProcessResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value if ($strServiceExceptionMessage -ne '') { throw "ProcessAuth: $strServiceExceptionMessage" } $formElements = ([XML]$SASProcessResponse.Content).GetElementsByTagName("input"); $authCode = "" foreach ($element in $formElements) { if ($element.Name -eq "code") { $authCode = $element.GetAttribute("value"); Write-Verbose $authCode } } $Body = @{"grant_type" = "authorization_code"; "scope" = $scopes; "client_id" = "$ClientId"; "code" = $authCode; "redirect_uri" = $RedirectURI } $tokenRequest = Invoke-RestMethod -Method Post -ContentType application/x-www-form-urlencoded -Uri "https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token" -Body $Body return $tokenRequest } } function Get-AccessTokenNonMFA{ [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [PSCredential] $Credential, [Parameter(Position = 1, Mandatory = $false)] [String] $ClientId = "95a1b05c-60f1-420d-a5b2-0cca170dfadc", [Parameter(Position = 2, Mandatory = $false)] [String] $RedirectURI = "https://login.microsoftonline.com/common/oauth2/nativeclient", [Parameter(Position = 3, Mandatory = $false)] [String] $scopes = "https://outlook.office.com/EWS.AccessAsUser.All" ) process { $domain = $Credential.UserName.Split('@')[1] $openidURL = "https://login.windows.net/$domain/v2.0/.well-known/openid-configuration" $TenantId = (Invoke-WebRequest -Uri $openidURL | ConvertFrom-Json).token_endpoint.Split('/')[3] $AuthURL = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=$ClientId&response_mode=form_post&response_type=code&redirect_uri=" + [System.Web.HttpUtility]::UrlEncode($RedirectURI) $StartLogon = Invoke-WebRequest -uri $AuthURL -SessionVariable 'AuthSession' $Context = [regex]::Match($StartLogon.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value $Flow = [regex]::Match($StartLogon.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value $Canary = [regex]::Match($StartLogon.RawContent,"`"canary`":`"(.*?)`"").Groups[1].Value $FBAAuthBody=@{ "login" = $Credential.UserName "loginFmt" = $Credential.UserName "i13"="0" "type"="11" "LoginOptions"="3" "passwd"= $Credential.GetNetworkCredential().password.ToString() "ps"="2" "flowToken"=$Flow "canary"=$Canary "ctx"=$Context "NewUser"="1" "fspost"="0" "i21"="0" "CookieDisclosure"="1" "IsFidoSupported"="1" "hpgrequestid"=(New-Guid).ToString() } if (($Context -eq '') -or ($flow -eq '')) { throw "Authorize: $strServiceExceptionMessage. $($StartLogon.RawContent)" } $FBAResponse = Invoke-WebRequest -Uri https://login.microsoftonline.com/common/login -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FBAAuthBody -WebSession $AuthSession $formElements = ([XML]$FBAResponse.Content).GetElementsByTagName("input"); $authCode = "" foreach ($element in $formElements) { if ($element.Name -eq "code") { $authCode = $element.GetAttribute("value"); Write-Verbose $authCode } } $Body = @{"grant_type" = "authorization_code"; "scope" = $scopes; "client_id" = "$ClientId"; "code" = $authCode; "redirect_uri" = $RedirectURI } $tokenRequest = Invoke-RestMethod -Method Post -ContentType application/x-www-form-urlencoded -Uri https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token -Body $Body return $tokenRequest } } <# .SYNOPSIS Generate a Time-Base One-Time Password based on RFC 6238. .DESCRIPTION This command uses the reference implementation of RFC 6238 to calculate a Time-Base One-Time Password. It bases on the HMAC SHA-1 hash function to generate a shot living One-Time Password. .INPUTS None. .OUTPUTS System.String. The one time password. .EXAMPLE PS C:\> Get-TimeBasedOneTimePassword -SharedSecret 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' Get the Time-Based One-Time Password at the moment. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/SecurityFever https://tools.ietf.org/html/rfc6238 #> function Get-TimeBasedOneTimePassword { [CmdletBinding()] [Alias('Get-TOTP')] param ( # Base 32 formatted shared secret (RFC 4648). [Parameter(Mandatory = $true)] [System.String] $SharedSecret, # The date and time for the target calculation, default is now (UTC). [Parameter(Mandatory = $false)] [System.DateTime] $Timestamp = (Get-Date).ToUniversalTime(), # Token length of the one-time password, default is 6 characters. [Parameter(Mandatory = $false)] [System.Int32] $Length = 6, # The hash method to calculate the TOTP, default is HMAC SHA-1. [Parameter(Mandatory = $false)] [System.Security.Cryptography.KeyedHashAlgorithm] $KeyedHashAlgorithm = (New-Object -TypeName 'System.Security.Cryptography.HMACSHA1'), # Baseline time to start counting the steps (T0), default is Unix epoch. [Parameter(Mandatory = $false)] [System.DateTime] $Baseline = '1970-01-01 00:00:00', # Interval for the steps in seconds (TI), default is 30 seconds. [Parameter(Mandatory = $false)] [System.Int32] $Interval = 30 ) # Generate the number of intervals between T0 and the timestamp (now) and # convert it to a byte array with the help of Int64 and the bit converter. $numberOfSeconds = ($Timestamp - $Baseline).TotalSeconds $numberOfIntervals = [Convert]::ToInt64([Math]::Floor($numberOfSeconds / $Interval)) $byteArrayInterval = [System.BitConverter]::GetBytes($numberOfIntervals) [Array]::Reverse($byteArrayInterval) # Use the shared secret as a key to convert the number of intervals to a # hash value. $KeyedHashAlgorithm.Key = Convert-Base32ToByte -Base32 $SharedSecret $hash = $KeyedHashAlgorithm.ComputeHash($byteArrayInterval) # Calculate offset, binary and otp according to RFC 6238 page 13. $offset = $hash[($hash.Length-1)] -band 0xf $binary = (($hash[$offset + 0] -band '0x7f') -shl 24) -bor (($hash[$offset + 1] -band '0xff') -shl 16) -bor (($hash[$offset + 2] -band '0xff') -shl 8) -bor (($hash[$offset + 3] -band '0xff')) $otpInt = $binary % ([Math]::Pow(10, $Length)) $otpStr = $otpInt.ToString().PadLeft($Length, '0') Write-Output $otpStr } function Convert-Base32ToByte { param ( [Parameter(Mandatory = $true)] [System.String] $Base32 ) # RFC 4648 Base32 alphabet $rfc4648 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' $bits = '' # Convert each Base32 character to the binary value between starting at # 00000 for A and ending with 11111 for 7. foreach ($char in $Base32.ToUpper().ToCharArray()) { $bits += [Convert]::ToString($rfc4648.IndexOf($char), 2).PadLeft(5, '0') } # Convert 8 bit chunks to bytes, ignore the last bits. for ($i = 0; $i -le ($bits.Length - 8); $i += 8) { [Byte] [Convert]::ToInt32($bits.Substring($i, 8), 2) } } <# get-graphTeamsGroup Retrieve the Teams Goup by its name #> function get-graphTeamsGroup { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [System.String]$groupName, [Parameter(Mandatory = $true)] [PSCustomObject]$token ) try { Write-Verbose "Getting Teams Group: $groupName" $team = @(use-graphApi -uri "groups?`$filter=displayName eq '$groupName'" -token $token) if ($team.count -eq 0) { throw [exception]"Teams Group with name '$groupName' was not found" } elseif ($team.Count -gt 1) { throw [exception]"Found $($team.Count) Teams Group with name '$groupName'. Should be unique. Aborting" } else { $team[0] } } catch { throw [Exception]"Failed to retrieve Teams Group $groupName : $($_.exception.message)" } } <# get-graphTeamsChannel Retrieves a Teams Channel Object by groupName + ChannelName #> function get-graphTeamsChannel { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [System.String]$groupName, [Parameter(Mandatory = $true)] [System.String]$channelName, [Parameter(Mandatory = $true)] [PSCustomObject]$token ) $team = get-graphTeamsGroup -groupName $groupName -token $token try { Write-Verbose "Getting Teams Channel: $channelName" $channel = @(use-graphApi -uri "teams/$($team.id)/channels?`$filter=displayName eq '$channelName'" -token $token) if ($channel.count -eq 0) { throw [exception]"Teams Channel with name '$channelName' was not found in group '$groupName'" } $channel[0] } catch { throw [Exception]"Failed to retrieve Teams Channel $channelName : $($_.exception.message)" } } <# get-graphTeamsFolderId Retrieves a Teams Librairy ItemID representing the given folder path #> function get-graphTeamsFolderId { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [System.String]$groupId, [Parameter(Mandatory = $true)] [System.String]$channelName, [System.String]$folderPath, [Parameter(Mandatory = $true)] [PSCustomObject]$token ) # Teams Get Sharepoint document library Write-Verbose "Getting Library" # Parse recursively the folder structure to retrieve the destination folder ID # ! There is a folder level between root and the folder structure in the channel named with the teams channel name ! $path = @() $path += $channelName $path += @($folderPath -split '/' |? {$_.trim() -ne ""}) $itemId = "root" foreach ($folder in $path) { Write-Verbose "- Searching $folder in $itemId" $itemId_temp = use-graphApi -token $token -uri "groups/$($groupId)/drive/items/$itemId/children?`$filter=name eq '$folder'" |select -ExpandProperty id if($itemid_temp) { $itemId = $itemId_temp } else { break } } Write-Verbose "- found folder with itemId $itemId" $itemId } <# publish-graphTeamsFile Uploads a file content to a Teams librairy ItemId #> function publish-graphTeamsFile { [cmdletbinding(DefaultParameterSetName = 'simple')] param( [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Teams Group ID")] [System.String]$groupId, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Folder ItemID to upload the file to")] [System.String]$itemId, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Teams Group Name")] [String]$groupName, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Teams Channel Name")] [String]$channelName, [Parameter(ParameterSetName="simple",Mandatory=$false,HelpMessage="Folder Path inside the channel to upload the file to")] [string]$folderPath, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Neme of the uploaded file")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Neme of the uploaded file")] [System.String]$fileName, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Content of the file")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Content of the file")] [System.String]$fileContent, [Parameter(ParameterSetName="direct",Mandatory=$false,HelpMessage="Content type of the file")] [Parameter(ParameterSetName="simple",Mandatory=$false,HelpMessage="Content type of the file")] [System.String]$fileContentType="text/plain", [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Graph Access Token object")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Graph Access Token object")] [PSCustomObject]$token ) try { if ($PSCmdlet.ParameterSetName -eq 'user') { Write-Verbose "Getting user token" $token = Get-AccessTokenMFA -OTP (Get-TimeBasedOneTimePassword -SharedSecret $mfaSecret) ` -Credential $credObject -Scopes 'https://graph.microsoft.com/.default' -ClientId $appId ` -verbose:$verbosepreference } if ($PSCmdlet.ParameterSetName -ne 'direct') { $group = get-graphTeamsGroup -groupName $groupName -token $token -verbose:$verbosepreference $itemId = get-graphTeamsFolderId -groupId $group.id -channelName $channelName -folderPath $folderPath -token $token -verbose:$verbosepreference } $uri = "groups/$($group.id)/drive/items/$($itemId):/$($filename):/content" Write-Verbose "Upload file : $uri" use-graphApi -uri $uri -token $token -raw -method PUT -body $fileContent -contentType $fileContentType -verbose:$verbosepreference } catch { throw [Exception]"Failed uploading file '$fileName' to '$uri' : $($_.exception.message)" } } <# get-graphSPSiteId Retrieves a SharePoint Online Site ID from it URL #> function get-graphSPSiteId { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [System.String]$spSiteUrl, [Parameter(Mandatory = $true)] [PSCustomObject]$token ) try { Write-verbose "Searching for target SharePoint site" # Get site by url $site = use-graphApi -token $token -uri "sites" -all |? { $_.weburl -like $spSiteUrl} $siteid = $site.sharepointids.siteid Write-verbose "- Sharepoint Site $spSiteUrl has ID : $siteid" $siteid } catch { throw [Exception]"Failed finding sharePoint site '$spSiteUrl' : $($_.exception.message)" } } <# get-graphSPFolderId Retrieve the itemId of the folder pointed to the provided path on the SharePoint Online site #> function get-graphSPFolderId { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [System.String]$siteid, [System.String]$folderPath, [Parameter(Mandatory = $true)] [PSCustomObject]$token ) $path = @($folderPath -split '/' |? {$_.trim() -ne ""}) $itemId = "root" foreach ($folder in $path) { Write-Verbose "- Searching $folder in $itemId" $itemId = use-graphApi -token $token -uri "sites/$siteid/drive/items/$itemId/children?`$filter=name eq '$folder'" |select -ExpandProperty id } Write-Verbose "- Storing file in site $siteid within folder item $itemId" } <# publish-graphSPFile Uploads a file content to a SharePoint Online folder ItemId #> function publish-graphSPFile { [cmdletbinding(DefaultParameterSetName = 'simple')] param( [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="SharePoint Online site ID")] [System.String]$siteid, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="SharePoint Online folder itemID")] [System.String]$itemId, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="SharePoint Online site ID")] [System.String]$spSiteUrl, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="SharePoint Online site ID")] [System.String]$spFolderPath, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Uploaded item's fileName")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Uploaded item's fileName")] [System.String]$fileName, [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Uploaded file's content")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Uploaded file's content")] [System.String]$fileContent, [Parameter(ParameterSetName="direct",Mandatory=$false,HelpMessage="File's content type")] [Parameter(ParameterSetName="simple",Mandatory=$false,HelpMessage="File's content type")] [System.String]$fileContentType="text/plain", [Parameter(ParameterSetName="direct",Mandatory=$true,HelpMessage="Graph API Access Token")] [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Graph API Access Token")] [PSCustomObject]$token ) if ($PSCmdlet.ParameterSetName -eq 'simple') { $siteId = get-graphSPSiteId -spSiteUrl $spSiteUrl -token $token $itemID = get-graphSPFolderId -siteId $siteId -folderPath $folderPath -token $token } try { $uri = "sites/$siteid/drive/items/$($itemId):/$($filename):/content" Write-Verbose "Upload file : $uri" use-graphApi -token $token -method PUT -uri $uri ` -body $fileContent -contentType $fileContentType -raw ` |select id, name, createdDatetime, webUrl } catch { throw [Exception]"Failed uploading file '$fileName' to '$uri' : $($_.exception.message)" } } function write-graphTeamsChannelMessage { [cmdletbinding(DefaultParameterSetName = 'simple')] param( [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Teams Group Name")] [String]$groupName, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Teams Channel Name")] [string]$channelName, [Parameter(ParameterSetName="simple",Mandatory=$false,HelpMessage="Message title")] [string]$subject, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="Message body")] [string]$body, [Parameter(ParameterSetName="simple",Mandatory=$false,HelpMessage="Message attachment")] [PSCustomObject]$uploadedFile, [Parameter(ParameterSetName="simple",Mandatory=$true,HelpMessage="GraphApi Access Token")] [PSCustomObject]$token ) try { $group = get-graphTeamsGroup -groupName $groupName -token $token -verbose:$verbosepreference $channel = get-graphTeamsChannel -groupName $groupName -channelName $channelName -token $token -verbose:$verbosepreference Write-Verbose "Writing to Channel $($channel.id)" $message = @{ body= @{ contentType= "html" content= $body } } if ($null -ne $subject) { $message.subject = $subject } if ($uploadedFile) { $itemRefId = ((($uploadedFile.etag) |ConvertFrom-Json) -split (',') |select -first 1) -replace '[{}]','' $message.attachments = @( @{ id= $itemRefId contentType= "reference" contentUrl= $uploadedFile.weburl name= $uploadedFile.name } ) $message.body.content += "<attachment id='$itemRefId'></attachment>" } use-graphApi -uri "/teams/$($group.id)/channels/$($channel.id)/messages" ` -token $token -method POST -body $message -raw } catch { [Exception]"Failed posting message: $($_.exception.message)" } } |