From dba2d7cf5ee96e24f2273fac44f16442d73ebb28 Mon Sep 17 00:00:00 2001 From: CyberMoloch Date: Sun, 28 Feb 2021 19:49:08 -0700 Subject: [PATCH 1/3] Code restructure and parameterize secrets --- OneDrive-AutoMapper.ps1 | 464 +++++++++++++++++++++------------------- 1 file changed, 246 insertions(+), 218 deletions(-) diff --git a/OneDrive-AutoMapper.ps1 b/OneDrive-AutoMapper.ps1 index 43219da..015fccb 100644 --- a/OneDrive-AutoMapper.ps1 +++ b/OneDrive-AutoMapper.ps1 @@ -23,7 +23,11 @@ For this script the following permissions must be assigned during the app regist Log file output to users %TEMP% folder (%temp%\OneDrive-AutoMapper_log.txt) .COPYRIGHT +Source: https://github.com/mardahl/OneDrive-AutoMapper + @michael_mardahl on Twitter (new followers appreciated) / https://www.iphase.dk +Modifications by cybermoloch@magitekai.com (@cyber_moloch on Twitter), https://github.com/cybermoloch + Some parts of the authentication functions have been heavily modified from their original state, initially provided by Microsoft as samples of Accessing Intune. Licensed under the MIT license. Please credit me if you fint this script useful and do some cool things with it. @@ -33,35 +37,15 @@ N.B. This is an updated version of my previous script "AutoMapUnifiedGroupDrives Use at your own risk, no warranty given! #> -#################################################### -# -# CONFIG -# -#################################################### - - #Required credentials - Get the client_id and client_secret from the app when creating it i Azure AD - $client_id = "88d56j01-856ja-tjdj-8166-9sdju56j56j" #App ID - $client_secret = "i3stryjtyjdtyjdtyz1Xhl:" #API Access Key Password - #Idealy you would secure this secret in some way, instead of having it here in clear text. - - #tenant_id can be read from the azure portal of your tenant (a.k.a Directory ID, shown when you do the App Registration) - $tenant_id = "1jd56j54-cj6d565-4d56je-9jd54-118f5d6j8" #Directory ID - - #Set to $true to delete leftover folders from previous syncs (if false, nothing wil be synced if the destination folder already exists) - $CleanupLeftovers = $true - #Enabling cleanup will also ensure that a folder get's re-added if a user removes it. - - #Seconds to wait between each mount - not having a delay can cause OneDrive to barf when adding multiple sync folders at once. (default: 3 sec) - $waitSec = 3 - - #List of site names to exclude from being added to OneDrive - #Just add the name of the site to this array, and remove the dummy entries. - $excludeSiteList = @("DummyDumDum","Blankorama","Nonenana") - - #Special params for some advanced modification - $global:sharedDocumentsName = "Shared Documents" #change if for some reason your tenant displays the shared documents folders with another name - $global:graphApiVersion = "v1.0" #should be "v1.0" - $VerbosePreference = "Continue" #change to SilentlyContinue to dial down the output. +param +( + [Parameter(Mandatory = $true)] + $TenantID, + [Parameter(Mandatory = $true)] + $ClientID, + [Parameter(Mandatory = $true)] + $ClientSecret +) #################################################### # @@ -85,15 +69,15 @@ Function Get-AuthToken { param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $TenantID, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $ClientID, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $ClientSecret ) - try{ + try { # Define parameters for Microsoft Graph access token retrieval $resource = "https://graph.microsoft.com" $authority = "https://login.microsoftonline.com/$TenantID" @@ -104,35 +88,34 @@ Function Get-AuthToken { $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing - Write-Host "Got new Access Token!" -ForegroundColor Green - Write-Host + Write-Information "Got new Access Token!" # If the accesstoken is valid then create the authentication header - if($response.access_token){ + if ($response.access_token) { - # Creating header for Authorization token + # Creating header for Authorization token - $authHeader = @{ - 'Content-Type'='application/json' - 'Authorization'="Bearer " + $response.access_token - 'ExpiresOn'=$response.expires_on + $authHeader = @{ + 'Content-Type' = 'application/json' + 'Authorization' = "Bearer " + $response.access_token + 'ExpiresOn' = $response.expires_on } - return $authHeader + return $authHeader } - else{ + else { - Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." - break + Write-Error "Authorization Access Token is null, check that the client_id and client_secret is correct..." + break } } - catch{ + catch { - FatalWebError -Exeption $_.Exception -Function "Get-AuthToken" + Out-FatalWebError -Exception $_.Exception -Function "Get-AuthToken" } @@ -154,11 +137,21 @@ Function Get-ValidToken { NAME: Get-ValidToken #> + param + ( + [Parameter(Mandatory = $true)] + $TenantID, + [Parameter(Mandatory = $true)] + $ClientID, + [Parameter(Mandatory = $true)] + $ClientSecret + ) + #Fixing client_secret illegal char (+), which do't go well with web requests - $client_secret = $($client_secret).Replace("+","%2B") + $ClientSecret = $($ClientSecret).Replace("+", "%2B") # Checking if authToken exists before running authentication - if($global:authToken){ + if ($global:authToken) { # Get current time in (UTC) UNIX format (and ditch the milliseconds) $CurrentTimeUnix = $((get-date ([DateTime]::UtcNow) -UFormat +%s)).split((Get-Culture).NumberFormat.NumberDecimalSeparator)[0] @@ -166,18 +159,16 @@ Function Get-ValidToken { # If the authToken exists checking when it expires (converted to minutes for readability in output) $TokenExpires = [MATH]::floor(([int]$authToken.ExpiresOn - [int]$CurrentTimeUnix) / 60) - if($TokenExpires -le 0){ + if ($TokenExpires -le 0) { - Write-Host "Authentication Token expired" $TokenExpires "minutes ago! - Requesting new one..." -ForegroundColor Green - $global:authToken = Get-AuthToken -TenantID $tenant_id -ClientID $client_id -ClientSecret $client_secret + Write-Information ("Authentication Token expired " + $TokenExpires + " minutes ago! - Requesting new one...") + $global:authToken = Get-AuthToken -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret - } - else{ - - Write-Host "Using valid Authentication Token that expires in" $TokenExpires "minutes..." -ForegroundColor Green - Write-Host + } + else { - } + Write-Information ("Using valid Authentication Token that expires in " + $TokenExpires + " minutes...") + } } @@ -186,7 +177,7 @@ Function Get-ValidToken { else { # Getting the authorization token - $global:authToken = Get-AuthToken -TenantID $tenant_id -ClientID $client_id -ClientSecret $client_secret + $global:authToken = Get-AuthToken -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret } @@ -194,7 +185,7 @@ Function Get-ValidToken { #################################################### -Function FatalWebError { +Function Out-FatalWebError { <# .SYNOPSIS @@ -202,38 +193,37 @@ Function FatalWebError { .DESCRIPTION Unwraps most of the exeptions details and gets the response codes from the web request, afterwards it stops script execution. .EXAMPLE - FatalWebError -Exception $_.Exception -Function "myFunctionName" + Out-FatalWebError -Exception $_.Exception -Function "myFunctionName" Shows the error message and the name of the function calling it. .NOTES - NAME: FatalWebError + NAME: Out-FatalWebError #> param ( - [Parameter(Mandatory=$true)] - $Exeption, # Should be the execption trace, you might try $_.Exception - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] + $Exception, # Should be the exception trace, you might try $_.Exception + [Parameter(Mandatory = $true)] $Function # Name of the function that calls this function (for readability) ) -#Handles errors for all my Try/Catch'es + #Handles errors for all my Try/Catch'es - $errorResponse = $Exeption.Response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($errorResponse) - $reader.BaseStream.Position = 0 - $reader.DiscardBufferedData() - $responseBody = $reader.ReadToEnd(); - Write-Host "Failed to execute Function : $Function" -f Red - Write-Host "Response content:`n$responseBody" -f Red - Write-Host "Request to $Uri failed with HTTP Status $($Exeption.Response.StatusCode) $($Exeption.Response.StatusDescription)" -f Red - write-host - break + $errorResponse = $Exeption.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($errorResponse) + $reader.BaseStream.Position = 0 + $reader.DiscardBufferedData() + $responseBody = $reader.ReadToEnd(); + Write-Error "Failed to execute Function : $Function" + Write-Error "Response content:`n$responseBody" + Write-Error "Request to $Uri failed with HTTP Status $($Exception.Response.StatusCode) $($Exception.Response.StatusDescription)" + break } #################################################### -Function Get-UnifiedGroups(){ +Function Get-UnifiedGroups() { <# .SYNOPSIS @@ -249,7 +239,7 @@ Function Get-UnifiedGroups(){ param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $UPN ) @@ -258,23 +248,17 @@ Function Get-UnifiedGroups(){ $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource" try { - Return (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value - } catch { - - FatalWebError -Exeption $_.Exception -Function "Get-UnifiedGroups" - + # Out-FatalWebError -Exception $_.Exception -Function "Get-UnifiedGroups" } - - } #################################################### -Function Get-UnifiedGroupDrive(){ +Function Get-UnifiedGroupDrive() { <# .SYNOPSIS @@ -290,7 +274,7 @@ Function Get-UnifiedGroupDrive(){ param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $GroupID ) @@ -305,7 +289,7 @@ Function Get-UnifiedGroupDrive(){ catch { - FatalWebError -Exeption $_.Exception -Function "Get-UnifiedGroupDrive" + Out-FatalWebError -Exception $_.Exception -Function "Get-UnifiedGroupDrive" } @@ -314,7 +298,7 @@ Function Get-UnifiedGroupDrive(){ ##################################################### -Function Get-UnifiedGroupDriveList(){ +Function Get-UnifiedGroupDriveList() { <# .SYNOPSIS @@ -331,7 +315,7 @@ Function Get-UnifiedGroupDriveList(){ param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $GroupID ) @@ -347,7 +331,7 @@ Function Get-UnifiedGroupDriveList(){ catch { - FatalWebError -Exeption $_.Exception -Function "Get-GroupDriveListID" + Out-FatalWebError -Exception $_.Exception -Function "Get-GroupDriveListID" } @@ -356,7 +340,7 @@ Function Get-UnifiedGroupDriveList(){ ##################################################### -Function Get-CurrentUserODInfo(){ +Function Get-CurrentUserODInfo() { <# .SYNOPSIS @@ -364,14 +348,14 @@ Function Get-CurrentUserODInfo(){ .DESCRIPTION The function parses the HKCU registry hive, matching certain propertied with the specified TenantID - Returning userEmail and UserFolder if a match is found .EXAMPLE - Get-CurrentUserODInfo -TennantID "00000000-0000000-0000000" + Get-CurrentUserODInfo -TenantID "00000000-0000000-0000000" .NOTES NAME: Get-CurrentUserODInfo #> param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $TenantID ) @@ -380,7 +364,7 @@ Function Get-CurrentUserODInfo(){ $ODFBaccounts = Get-ChildItem -Path $ODFBregPath #resetting ODUserEmail in case script is run multiple times in same session - $ODuserEmail -eq $null + $ODuserEmail = $null #Find the correct OneDrive Account for this tennant, in case the user has multiple OD accounts. foreach ($Account in $ODFBaccounts) { @@ -389,18 +373,18 @@ Function Get-CurrentUserODInfo(){ $ODTenant = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty ConfiguredTenantId - if ($ODTenant -eq $TenantID) { + if ($ODTenant -eq $TenantID) { - $ODuserEmail = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty UserEmail - $ODuserFolder = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty UserFolder - $ODuserTenantName = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty DisplayName - #Getting a list of Existing MountPoints that are synced with the OneDrive Client (key might not exist is no drives are syncing, so we silently continue on any error. - $MountPoints = Get-Item -Path "Registry::$($Account.Name)\Tenants\$ODuserTenantName" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Property - } + $ODuserEmail = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty UserEmail + $ODuserFolder = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty UserFolder + $ODuserTenantName = Get-ItemProperty -Path "Registry::$($Account.Name)" | Select-Object -ExpandProperty DisplayName + #Getting a list of Existing MountPoints that are synced with the OneDrive Client (key might not exist is no drives are syncing, so we silently continue on any error. + $MountPoints = Get-Item -Path "Registry::$($Account.Name)\Tenants\$ODuserTenantName" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Property + } } } - if ($ODuserEmail -eq $null) { + if ($null -eq $ODuserEmail) { Write-Error "No configured OneDrive accounts found for the configured Tenant ID! Script will exit now." exit 1 @@ -410,10 +394,10 @@ Function Get-CurrentUserODInfo(){ #Building hashtable with our aquired OneDrive info and returning it to the caller. $ODinfo = @{ - 'Email'=$ODuserEmail - 'Folder'=$ODuserFolder - 'TenantName'=$ODuserTenantName - 'MountPoints'=$MountPoints + 'Email' = $ODuserEmail + 'Folder' = $ODuserFolder + 'TenantName' = $ODuserTenantName + 'MountPoints' = $MountPoints } return $ODinfo @@ -423,60 +407,60 @@ Function Get-CurrentUserODInfo(){ ###################################################### -Function Get-GroupODSyncURL(){ +Function Get-GroupODSyncURL() { <# .SYNOPSIS - This function is used to find the OneDrive user email and folder of the currently logged in user, matching a specific Azure AD Tennant ID + This function is used to find the OneDrive URL used for syncing. .DESCRIPTION - The function parses the HKCU registry hive, matching certain propertied with the specified TenantID - Returning userEmail and UserFolder if a match is found + The function parses the groups available to the user to return th e list of URLs to use in the sync. .EXAMPLE - Get-CurrentUserODInfo -TennantID "00000000-0000000-0000000" + Get-GroupODSyncURL -GroupID '00000000-0000-0000-0000-000000000000' -UPN 'user@contoso.com' .NOTES - NAME: Get-CurrentUserODInfo + NAME: Get-GroupODSyncURL #> param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $GroupID, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $UPN ) - #Executing other functions in order to get the ID's we require to build the odopen:// URL. - $DriveInfo = Get-UnifiedGroupDrive -GroupID $GroupID - $ListInfo = Get-UnifiedGroupDriveList -GroupID $GroupID - - #Building our odopen:// URL from the information we have gathered, and encoding it correctly so OneDrive wont barf when we feed it. - $siteid = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo.id.Split(',')[1])}") - $webid = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo.id.Split(',')[2])}") - $upn = [System.Web.HttpUtility]::UrlEncode($UPN) - $webURL = [System.Web.HttpUtility]::UrlEncode($DriveInfo.webUrl) - $webtitle = [System.Web.HttpUtility]::UrlEncode($DriveInfo.DisplayName).Replace("+","%20") - - # Checking to see if this library is excluded - foreach ($siteName in $excludeSiteList) { - if ($DriveInfo.name -eq $siteName) { - return $false - } + #Executing other functions in order to get the ID's we require to build the odopen:// URL. + $DriveInfo = Get-UnifiedGroupDrive -GroupID $GroupID + $ListInfo = Get-UnifiedGroupDriveList -GroupID $GroupID + + #Building our odopen:// URL from the information we have gathered, and encoding it correctly so OneDrive wont barf when we feed it. + $siteid = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo.id.Split(',')[1])}") + $webid = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo.id.Split(',')[2])}") + $upn = [System.Web.HttpUtility]::UrlEncode($UPN) + $webURL = [System.Web.HttpUtility]::UrlEncode($DriveInfo.webUrl) + $webtitle = [System.Web.HttpUtility]::UrlEncode($DriveInfo.DisplayName).Replace("+", "%20") + + # Checking to see if this library is excluded + foreach ($siteName in $excludeSiteList) { + if ($DriveInfo.name -eq $siteName) { + return $false } + } - #Finding the correct ListID for the "Shared Documents" library - $sharedDocumentsListId = $ListInfo | Where-Object Name -Match $sharedDocumentsName | Select-Object -ExpandProperty id - $listid = [System.Web.HttpUtility]::UrlEncode($sharedDocumentsListId) + #Finding the correct ListID for the "Shared Documents" library + $sharedDocumentsListId = $ListInfo | Where-Object Name -Match $sharedDocumentsName | Select-Object -ExpandProperty id + $listid = [System.Web.HttpUtility]::UrlEncode($sharedDocumentsListId) - #Returning the correctly assembled ODOPEN URL - return "odopen://sync/?siteId=$siteid&webId=$webid&listId=$listid&userEmail=$upn&webUrl=$webURL&webtitle=$webtitle" + #Returning the correctly assembled ODOPEN URL + return "odopen://sync/?siteId=$siteid&webId=$webid&listId=$listid&userEmail=$upn&webUrl=$webURL&webtitle=$webtitle" - #If you want a custom suffix for your list titles, then you can use this retunr string instead, remember to outcomment the other one above! - #$listtitle = [System.Web.HttpUtility]::UrlEncode("Documents") - #return "odopen://sync/?siteId=$siteid&webId=$webid&listId=$listid&userEmail=$upn&webUrl=$webURL&webtitle=$webtitle&listtitle=$listtitle" + #If you want a custom suffix for your list titles, then you can use this retunr string instead, remember to outcomment the other one above! + #$listtitle = [System.Web.HttpUtility]::UrlEncode("Documents") + #return "odopen://sync/?siteId=$siteid&webId=$webid&listId=$listid&userEmail=$upn&webUrl=$webURL&webtitle=$webtitle&listtitle=$listtitle" } ###################################################### -Function Get-DriveMembers(){ +Function Get-DriveMembers() { <# .SYNOPSIS @@ -492,7 +476,7 @@ Function Get-DriveMembers(){ param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] $GroupID ) @@ -507,7 +491,7 @@ Function Get-DriveMembers(){ catch { - FatalWebError -Exeption $_.Exception -Function "Get-Drive" + Out-FatalWebError -Exception $_.Exception -Function "Get-Drive" } @@ -515,7 +499,7 @@ Function Get-DriveMembers(){ ###################################################### -function WaitForOneDrive () { +function Wait-OneDriveClient () { <# .SYNOPSIS @@ -523,13 +507,19 @@ function WaitForOneDrive () { .DESCRIPTION The function poll's for the OneDrive process every second, and will resume script execution, once it's running .EXAMPLE - WaitForOneDrive + Wait-OneDriveClient .NOTES NAME: WaitforOneDrive #> + [CmdletBinding()] + param ( + [Parameter(Position = 0)] + [Int32][ValidateRange(1, 3000)] + $MaxWaitSec = '300' #maximum number of seconds we are willing to wait for the OneDrive Process. (not an exact counter, might be a bit longer) + ) + $started = $false - $maxWaitSec = 300 #maximum number of seconds we are willing to wait for the OneDrive Process. (not an exact counter, might be a bit longer) $wait = 0 #Initial Wait counter Do { @@ -537,17 +527,18 @@ function WaitForOneDrive () { $status = Get-Process OneDrive -ErrorAction SilentlyContinue #Looking for the OneDrive Process If (!($status)) { - Write-Output 'Waiting for OneDrive to start...' + Write-Information 'Waiting for OneDrive to start...' Start-Sleep -Seconds 1 - } Else { - Write-Output 'OneDrive has started yo!' + } + Else { + Write-Information 'OneDrive has started yo!' $started = $true } $wait++ #increase wait counter - If ($wait -eq $maxWaitSec) { - Write-Output "Failed to find OneDrive Process. Exiting Script!" + If ($wait -eq $MaxWaitSec) { + Write-Information "Failed to find OneDrive Process. Exiting Script!" Exit } @@ -555,89 +546,126 @@ function WaitForOneDrive () { Until ( $started ) } +function Mount-OneDriveUnifiedGroups { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $TenantID, + #tenant_id can be read from the azure portal of your tenant (a.k.a Directory ID, shown when you do the App Registration) + #Required credentials - Get the client_id and client_secret from the app when creating it i Azure AD + #Idealy you would secure this secret in some way, instead of having it here in clear text. + [Parameter(Mandatory = $true)] + $ClientID, + [Parameter(Mandatory = $true)] + $ClientSecret, + [Parameter(Mandatory = $false)] + $CleanupLeftovers = $true, + #Set to $true to delete leftover folders from previous syncs (if false, nothing wil be synced if the destination folder already exists) + #Enabling cleanup will also ensure that a folder get's re-added if a user removes it. + [Parameter(Mandatory = $false)] + [Int32][ValidateRange(1, 300)] + $WaitSec = 10, #Seconds to wait between each mount - not having a delay can cause OneDrive to barf when adding multiple sync folders at once. + [Parameter(Mandatory = $false)] + [Array]$ExcludeSiteList, + #List of site names to exclude from being added to OneDrive + #Just add the name of the site to this array, and remove the dummy entries. + # $excludeSiteList = @("DummyDumDum", "Blankorama", "Nonenana") + [Parameter(Mandatory = $false)] + $VerbosePreference = 'Continue' #change to SilentlyContinue to dial down the output. + ) + + begin { + #Special params for some advanced modification + $global:sharedDocumentsName = "Shared Documents" #change if for some reason your tenant displays the shared documents folders with another name + $global:graphApiVersion = "v1.0" #should be "v1.0" + } + + process { + Start-Transcript "$env:TEMP\OneDrive-AutoMapper_log.txt" -Force + # Wait for OneDrive Process + Wait-OneDriveClient + # Getting OneDrive data for currently logged in user, and matching it with the configured Tenant ID + $OneDrive = Get-CurrentUserODInfo -TenantID $TenantID -##################################################### -# -# SCRIPT EXECUTION -# -###################################################### - -# Starting log -Start-Transcript "$env:TEMP\OneDrive-AutoMapper_log.txt" -Force - -# Wait for OneDrive Process -WaitForOneDrive - -# Getting OneDrive data for currently logged in user, and matching it with the configured Tenant ID -$OneDrive = Get-CurrentUserODInfo -TenantID $tenant_id - -# Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. -Get-ValidToken + # Calling Microsoft to see if they will give us access with the parameters defined in the config section of this script. + Get-ValidToken -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret -# Getting a list of all O365 Unified Groups -$allUnifiedGroups = $null -$allUnifiedGroups = Get-UnifiedGroups -UPN $($OneDrive.Email) + # Getting a list of all O365 Unified Groups + $allUnifiedGroups = $null + $allUnifiedGroups = Get-UnifiedGroups -UPN $($OneDrive.Email) -# Getting OneDrive Sync URL's for all Unified Groups + # Getting OneDrive Sync URL's for all Unified Groups -Write-Host "Mounting OneDrive all Unified Groups in Tenant ($($OneDrive.TenantName)) that is accessible by $($OneDrive.Email)" -ForegroundColor Yellow -Write-Host + Write-Information "Mounting OneDrive all Unified Groups in Tenant ($($OneDrive.TenantName)) that is accessible by $($OneDrive.Email)" -foreach ($Group in $allUnifiedGroups) { + foreach ($Group in $allUnifiedGroups) { - #Skip if group is not unified - if (!$($group.groupTypes -like "Unified*")){Continue} + #Skip if group is not unified + if (!$($group.groupTypes -like "Unified*")) { Continue } - # Validate that the users is not already Syncing the Drive - if ($($OneDrive.MountPoints) -match [Regex]::Escape("\$($Group.displayName) -")){ - Write-Host "The site ($($Group.displayName)) is already synced! Skipping..." -ForegroundColor Yellow - Write-Host - continue #skip this group and go to the next group - } + # Validate that the users is not already Syncing the Drive + if ($($OneDrive.MountPoints) -match [Regex]::Escape("\$($Group.displayName) -")) { + Write-Information "The site ($($Group.displayName)) is already synced! Skipping..." + continue #skip this group and go to the next group + } - Write-Host "Attempting to sync the drive ($($Group.displayName))..." -ForegroundColor Yellow + Write-Information "Attempting to sync the drive ($($Group.displayName))..." - # Getting the OneDrive Sync URL for the Group Drive - $ODsyncURL = Get-GroupODSyncURL -GroupID $Group.id -UPN $OneDrive.Email + # Getting the OneDrive Sync URL for the Group Drive + $ODsyncURL = Get-GroupODSyncURL -GroupID $Group.id -UPN $OneDrive.Email - #Skipping this Library if it has been excluded - if ($ODsyncURL -eq $false) { - Write-Host "This drive is on the excluded sites list! Skipping..." -ForegroundColor DarkYellow - continue #skip this group and go to the next group - } else { - Write-Verbose $Group.displayName - Write-Verbose $ODsyncURL - } - - # Check for leftover folders, and start sync if none found, else cleanup and start sync. - $UserHomePath = join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH - $BusinessPath = Join-Path -Path $UserHomePath -ChildPath $($OneDrive.TenantName) + #Skipping this Library if it has been excluded + if ($ODsyncURL -eq $false) { + Write-Information "This drive is on the excluded sites list! Skipping..." + continue #skip this group and go to the next group + } + else { + Write-Verbose $Group.displayName + Write-Verbose $ODsyncURL + } - try { - $syncFolders = Get-ChildItem $BusinessPath -ErrorAction Stop - foreach ($folder in $syncFolders) { - if (($($folder.Name) -like ("$($Group.displayName) - *")) -and ($CleanupLeftovers -eq $true)) { - $localSyncPath = Join-Path -Path $BusinessPath -ChildPath $($folder.Name) -ErrorAction SilentlyContinue - "Found existing sync for folder for : $($Group.displayName)" - Write-Host "Leftover Folder Found for $localSyncPath" -ForegroundColor Red - Write-Host '$CleanupLeftovers is set to true - Deleting old folder and starting sync' -ForegroundColor Yellow - Remove-Item -Path $localSyncPath -Force -Recurse -ErrorAction SilentlyContinue - } elseif ($($folder.Name) -like ("$($Group.displayName) - *")) { - Write-Host "Existing folder found for : '$($Group.displayName)'" -ForegroundColor Red - Write-Host "OneDrive will complain because 'CleanupLeftovers' is not set to True." -ForegroundColor Red + # Check for leftover folders, and start sync if none found, else cleanup and start sync. + $UserHomePath = join-Path -Path $env:HOMEDRIVE -ChildPath $env:HOMEPATH + $BusinessPath = Join-Path -Path $UserHomePath -ChildPath $($OneDrive.TenantName) + + try { + $syncFolders = Get-ChildItem $BusinessPath -ErrorAction Stop + foreach ($folder in $syncFolders) { + if (($($folder.Name) -like ("$($Group.displayName) - *")) -and ($CleanupLeftovers -eq $true)) { + $localSyncPath = Join-Path -Path $BusinessPath -ChildPath $($folder.Name) -ErrorAction SilentlyContinue + "Found existing sync for folder for : $($Group.displayName)" + Write-Information "Leftover Folder Found for $localSyncPath" + Write-Information '$CleanupLeftovers is set to true - Deleting old folder and starting sync' + Remove-Item -Path $localSyncPath -Force -Recurse -ErrorAction SilentlyContinue + } + elseif ($($folder.Name) -like ("$($Group.displayName) - *")) { + Write-Information "Existing folder found for : '$($Group.displayName)'" + Write-Information "OneDrive will complain because 'CleanupLeftovers' is not set to True." + } + } + } + catch { + Write-Verbose "No previous sync folders found for this user in path : $BusinessPath" } + + Write-Information "The site ($($Group.displayName)) is NOT synced! Adding to OneDrive client..." + Start-Process $ODsyncURL # Sending site info to the OneDrive client + Start-Sleep -Seconds $waitSec + } - } catch { - Write-Verbose "No previous sync folders found for this user in path : $BusinessPath" + Stop-Transcript } - - Write-Host - Write-Host "The site ($($Group.displayName)) is NOT synced! Adding to OneDrive client..." -ForegroundColor Yellow - Start $ODsyncURL # Sending site info to the OneDrive client - Sleep -Seconds $waitSec - Write-Host + end { + + } } -#Ending log -Stop-Transcript + +##################################################### +# +# SCRIPT EXECUTION +# +###################################################### + + Mount-OneDriveUnifiedGroups -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret \ No newline at end of file From 54e4aead595dabe95b3ff3fee9860ed9d8e0405f Mon Sep 17 00:00:00 2001 From: CyberMoloch Date: Sun, 28 Feb 2021 20:05:02 -0700 Subject: [PATCH 2/3] Added Invoke-AsCurrentUser to allow run as SYSTEM --- OneDrive-AutoMapper.ps1 | 467 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 466 insertions(+), 1 deletion(-) diff --git a/OneDrive-AutoMapper.ps1 b/OneDrive-AutoMapper.ps1 index 015fccb..ee11b9b 100644 --- a/OneDrive-AutoMapper.ps1 +++ b/OneDrive-AutoMapper.ps1 @@ -26,6 +26,7 @@ Log file output to users %TEMP% folder (%temp%\OneDrive-AutoMapper_log.txt) Source: https://github.com/mardahl/OneDrive-AutoMapper @michael_mardahl on Twitter (new followers appreciated) / https://www.iphase.dk +Invoke-AsCurrentUser from https://github.com/KelvinTegelaar/RunAsUser Modifications by cybermoloch@magitekai.com (@cyber_moloch on Twitter), https://github.com/cybermoloch Some parts of the authentication functions have been heavily modified from their original state, initially provided by Microsoft as samples of Accessing Intune. @@ -662,10 +663,474 @@ function Mount-OneDriveUnifiedGroups { } } +function Invoke-AsCurrentUser { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [scriptblock] + $ScriptBlock, + [Parameter(Mandatory = $false)] + [switch]$NoWait, + [Parameter(Mandatory = $false)] + [switch]$UseWindowsPowerShell, + [Parameter(Mandatory = $false)] + [switch]$NonElevatedSession, + [Parameter(Mandatory = $false)] + [switch]$Visible + ) + + begin { + $script:source = @" +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace RunAsUser +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public int cb; + public String lpReserved; + public String lpDesktop; + public String lpTitle; + public uint dwX; + public uint dwY; + public uint dwXSize; + public uint dwYSize; + public uint dwXCountChars; + public uint dwYCountChars; + public uint dwFillAttribute; + public uint dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WTS_SESSION_INFO + { + public readonly UInt32 SessionID; + + [MarshalAs(UnmanagedType.LPStr)] + public readonly String pWinStationName; + + public readonly WTS_CONNECTSTATE_CLASS State; + } + } + + internal class NativeMethods + { + [DllImport("kernel32", SetLastError=true)] + public static extern int WaitForSingleObject( + IntPtr hHandle, + int dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle( + IntPtr hSnapshot); + + [DllImport("userenv.dll", SetLastError = true)] + public static extern bool CreateEnvironmentBlock( + ref IntPtr lpEnvironment, + SafeHandle hToken, + bool bInherit); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CreateProcessAsUserW( + SafeHandle hToken, + String lpApplicationName, + StringBuilder lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandle, + uint dwCreationFlags, + IntPtr lpEnvironment, + String lpCurrentDirectory, + ref NativeHelpers.STARTUPINFO lpStartupInfo, + out NativeHelpers.PROCESS_INFORMATION lpProcessInformation); + + [DllImport("userenv.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DestroyEnvironmentBlock( + IntPtr lpEnvironment); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool DuplicateTokenEx( + SafeHandle ExistingTokenHandle, + uint dwDesiredAccess, + IntPtr lpThreadAttributes, + SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, + TOKEN_TYPE TokenType, + out SafeNativeHandle DuplicateTokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool GetTokenInformation( + SafeHandle TokenHandle, + uint TokenInformationClass, + SafeMemoryBuffer TokenInformation, + int TokenInformationLength, + out int ReturnLength); + + [DllImport("wtsapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool WTSEnumerateSessions( + IntPtr hServer, + int Reserved, + int Version, + ref IntPtr ppSessionInfo, + ref int pCount); + + [DllImport("wtsapi32.dll")] + public static extern void WTSFreeMemory( + IntPtr pMemory); + + [DllImport("kernel32.dll")] + public static extern uint WTSGetActiveConsoleSessionId(); + + [DllImport("Wtsapi32.dll", SetLastError = true)] + public static extern bool WTSQueryUserToken( + uint SessionId, + out SafeNativeHandle phToken); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeNativeHandle() : base(true) { } + public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; } + + protected override bool ReleaseHandle() + { + return NativeMethods.CloseHandle(handle); + } + } + + internal enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous = 0, + SecurityIdentification = 1, + SecurityImpersonation = 2, + SecurityDelegation = 3, + } + + internal enum SW + { + SW_HIDE = 0, + SW_SHOWNORMAL = 1, + SW_NORMAL = 1, + SW_SHOWMINIMIZED = 2, + SW_SHOWMAXIMIZED = 3, + SW_MAXIMIZE = 3, + SW_SHOWNOACTIVATE = 4, + SW_SHOW = 5, + SW_MINIMIZE = 6, + SW_SHOWMINNOACTIVE = 7, + SW_SHOWNA = 8, + SW_RESTORE = 9, + SW_SHOWDEFAULT = 10, + SW_MAX = 10 + } + + internal enum TokenElevationType + { + TokenElevationTypeDefault = 1, + TokenElevationTypeFull, + TokenElevationTypeLimited, + } + + internal enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation = 2 + } + + internal enum WTS_CONNECTSTATE_CLASS + { + WTSActive, + WTSConnected, + WTSConnectQuery, + WTSShadow, + WTSDisconnected, + WTSIdle, + WTSListen, + WTSReset, + WTSDown, + WTSInit + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public static class ProcessExtensions + { + #region Win32 Constants + + private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; + private const int CREATE_NO_WINDOW = 0x08000000; + + private const int CREATE_NEW_CONSOLE = 0x00000010; + + private const uint INVALID_SESSION_ID = 0xFFFFFFFF; + private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero; + + #endregion + + // Gets the user token from the currently active session + private static SafeNativeHandle GetSessionUserToken(bool elevated) + { + var activeSessionId = INVALID_SESSION_ID; + var pSessionInfo = IntPtr.Zero; + var sessionCount = 0; + + // Get a handle to the user access token for the current active session. + if (NativeMethods.WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount)) + { + try + { + var arrayElementSize = Marshal.SizeOf(typeof(NativeHelpers.WTS_SESSION_INFO)); + var current = pSessionInfo; + + for (var i = 0; i < sessionCount; i++) + { + var si = (NativeHelpers.WTS_SESSION_INFO)Marshal.PtrToStructure( + current, typeof(NativeHelpers.WTS_SESSION_INFO)); + current = IntPtr.Add(current, arrayElementSize); + + if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) + { + activeSessionId = si.SessionID; + break; + } + } + } + finally + { + NativeMethods.WTSFreeMemory(pSessionInfo); + } + } + + // If enumerating did not work, fall back to the old method + if (activeSessionId == INVALID_SESSION_ID) + { + activeSessionId = NativeMethods.WTSGetActiveConsoleSessionId(); + } + + SafeNativeHandle hImpersonationToken; + if (!NativeMethods.WTSQueryUserToken(activeSessionId, out hImpersonationToken)) + { + throw new Win32Exception("WTSQueryUserToken failed to get access token."); + } + + using (hImpersonationToken) + { + // First see if the token is the full token or not. If it is a limited token we need to get the + // linked (full/elevated token) and use that for the CreateProcess task. If it is already the full or + // default token then we already have the best token possible. + TokenElevationType elevationType = GetTokenElevationType(hImpersonationToken); + + if (elevationType == TokenElevationType.TokenElevationTypeLimited && elevated == true) + { + using (var linkedToken = GetTokenLinkedToken(hImpersonationToken)) + return DuplicateTokenAsPrimary(linkedToken); + } + else + { + return DuplicateTokenAsPrimary(hImpersonationToken); + } + } + } + + public static int StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true,int wait = -1, bool elevated = true) + { + using (var hUserToken = GetSessionUserToken(elevated)) + { + var startInfo = new NativeHelpers.STARTUPINFO(); + startInfo.cb = Marshal.SizeOf(startInfo); + + uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW); + startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE); + //startInfo.lpDesktop = "winsta0\\default"; + + IntPtr pEnv = IntPtr.Zero; + if (!NativeMethods.CreateEnvironmentBlock(ref pEnv, hUserToken, false)) + { + throw new Win32Exception("CreateEnvironmentBlock failed."); + } + try + { + StringBuilder commandLine = new StringBuilder(cmdLine); + var procInfo = new NativeHelpers.PROCESS_INFORMATION(); + + if (!NativeMethods.CreateProcessAsUserW(hUserToken, + appPath, // Application Name + commandLine, // Command Line + IntPtr.Zero, + IntPtr.Zero, + false, + dwCreationFlags, + pEnv, + workDir, // Working directory + ref startInfo, + out procInfo)) + { + throw new Win32Exception("CreateProcessAsUser failed."); + } + + try + { + NativeMethods.WaitForSingleObject( procInfo.hProcess, wait); + return procInfo.dwProcessId; + } + finally + { + NativeMethods.CloseHandle(procInfo.hThread); + NativeMethods.CloseHandle(procInfo.hProcess); + } + } + finally + { + NativeMethods.DestroyEnvironmentBlock(pEnv); + } + } + } + + private static SafeNativeHandle DuplicateTokenAsPrimary(SafeHandle hToken) + { + SafeNativeHandle pDupToken; + if (!NativeMethods.DuplicateTokenEx(hToken, 0, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, + TOKEN_TYPE.TokenPrimary, out pDupToken)) + { + throw new Win32Exception("DuplicateTokenEx failed."); + } + + return pDupToken; + } + + private static TokenElevationType GetTokenElevationType(SafeHandle hToken) + { + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, 18)) + { + return (TokenElevationType)Marshal.ReadInt32(tokenInfo.DangerousGetHandle()); + } + } + + private static SafeNativeHandle GetTokenLinkedToken(SafeHandle hToken) + { + using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, 19)) + { + return new SafeNativeHandle(Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle())); + } + } + + private static SafeMemoryBuffer GetTokenInformation(SafeHandle hToken, uint infoClass) + { + int returnLength; + bool res = NativeMethods.GetTokenInformation(hToken, infoClass, new SafeMemoryBuffer(IntPtr.Zero), 0, + out returnLength); + int errCode = Marshal.GetLastWin32Error(); + if (!res && errCode != 24 && errCode != 122) // ERROR_INSUFFICIENT_BUFFER, ERROR_BAD_LENGTH + { + throw new Win32Exception(errCode, String.Format("GetTokenInformation({0}) failed to get buffer length", infoClass)); + } + + SafeMemoryBuffer tokenInfo = new SafeMemoryBuffer(returnLength); + if (!NativeMethods.GetTokenInformation(hToken, infoClass, tokenInfo, returnLength, out returnLength)) + throw new Win32Exception(String.Format("GetTokenInformation({0}) failed", infoClass)); + + return tokenInfo; + } + } +} +"@ + } + + process { + if (!("RunAsUser.ProcessExtensions" -as [type])) { + Add-Type -TypeDefinition $script:source -Language CSharp + } + $ExpandedScriptBlock = $ExecutionContext.InvokeCommand.ExpandString($ScriptBlock) + $encodedcommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($ExpandedScriptBlock)) + $privs = whoami /priv /fo csv | ConvertFrom-Csv | Where-Object { $_.'Privilege Name' -eq 'SeDelegateSessionUserImpersonatePrivilege' } + if ($privs.State -eq "Disabled") { + Write-Error -Message "Not running with correct privilege. You must run this script as system or have the SeDelegateSessionUserImpersonatePrivilege token." + return + } + else { + try { + # Use the same PowerShell executable as the one that invoked the function, Unless -UseWindowsPowerShell is defined + + if (!$UseWindowsPowerShell) { $pwshPath = (Get-Process -Id $pid).Path } else { $pwshPath = "$($ENV:windir)\system32\WindowsPowerShell\v1.0\powershell.exe" } + if ($NoWait) { $ProcWaitTime = 1 } else { $ProcWaitTime = -1 } + if ($NonElevatedSession) { $RunAsAdmin = $false } else { $RunAsAdmin = $true } + [RunAsUser.ProcessExtensions]::StartProcessAsCurrentUser( + $pwshPath, "`"$pwshPath`" -ExecutionPolicy Bypass -Window Normal -EncodedCommand $($encodedcommand)", + (Split-Path $pwshPath -Parent), $Visible, $ProcWaitTime, $RunAsAdmin) + } + catch { + Write-Error -Message "Could not execute as currently logged on user: $($_.Exception.Message)" -Exception $_.Exception + return + } + } + } +} + ##################################################### # # SCRIPT EXECUTION # ###################################################### - Mount-OneDriveUnifiedGroups -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret \ No newline at end of file +$runningContext = [System.Security.Principal.WindowsIdentity]::GetCurrent() +# Checks running if as SYSTEM (by SID) and runs it as the user instead +If ($runningContext.User -eq 'S-1-5-18') { + $ScriptBlock = { + & $PSScriptRoot\OneDrive-AutoMapper.ps1 -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret + } + Invoke-AsCurrentUser -UseWindowsPowerShell -NonElevatedSession -Visible -ScriptBlock $ScriptBlock +} +else { + Mount-OneDriveUnifiedGroups -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret +} \ No newline at end of file From ca5a52f300fef68597c0afa3f2f2beca44c3b3f6 Mon Sep 17 00:00:00 2001 From: CyberMoloch Date: Sun, 28 Feb 2021 20:20:47 -0700 Subject: [PATCH 3/3] Remove -Visible that was left for debugging --- OneDrive-AutoMapper.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneDrive-AutoMapper.ps1 b/OneDrive-AutoMapper.ps1 index ee11b9b..624134e 100644 --- a/OneDrive-AutoMapper.ps1 +++ b/OneDrive-AutoMapper.ps1 @@ -1129,7 +1129,7 @@ If ($runningContext.User -eq 'S-1-5-18') { $ScriptBlock = { & $PSScriptRoot\OneDrive-AutoMapper.ps1 -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret } - Invoke-AsCurrentUser -UseWindowsPowerShell -NonElevatedSession -Visible -ScriptBlock $ScriptBlock + Invoke-AsCurrentUser -UseWindowsPowerShell -NonElevatedSession -ScriptBlock $ScriptBlock } else { Mount-OneDriveUnifiedGroups -TenantID $TenantID -ClientID $ClientID -ClientSecret $ClientSecret