Last active
January 2, 2018 13:49
-
-
Save joerodgers/c72c8bcc969866d09278003ba33dc819 to your computer and use it in GitHub Desktop.
Adds Domain Group(s) and Permission Levels to SharePoint Online Sites (from CSV file)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add-Type -Path "C:\Microsoft.SharePointOnline.CSOM.16.1.6008.1200\lib\net45\Microsoft.SharePoint.Client.dll" | |
Add-Type -Path "C:\Microsoft.SharePointOnline.CSOM.16.1.6008.1200\lib\net45\Microsoft.SharePoint.Client.Runtime.dll" | |
$script:TRACE_LOG_PATH = "C:\_temp\permissions_$(Get-Date -Format "MMddyyyy").csv" | |
$inputFile = "C:\_temp\missing-perms.csv" | |
$credential = Get-Credential | |
function Write-TraceLogEntry | |
{ | |
<# | |
.Synopsis | |
Writes the specified message to a log file. The log file is path is either specified in the cmdlet or by the global variable $Global:TRACE_LOG_PATH | |
.EXAMPLE | |
Write-TraceLogEntry -Message "Log this to the file" -TraceLevel "Verbose" -Correlation "bad3f4e2-27c9-43ec-af16-e77fe9001ba6" | |
.EXAMPLE | |
Write-TraceLogEntry -Message "Log this to the file" -TraceLevel "Verbose" -Correlation "bad3f4e2-27c9-43ec-af16-e77fe9001ba6" -LogPath "C:\Temp\LogFile.log" | |
#> | |
[cmdletbinding()] | |
param | |
( | |
[parameter(Mandatory=$true)][string]$Message, | |
[parameter(Mandatory=$true)][ValidateSet("Verbose", "Low", "Medium", "High", "Critical")][string]$TraceLevel, | |
[parameter(Mandatory=$false)][string]$LogPath = $script:TRACE_LOG_PATH, | |
[parameter(Mandatory=$false)][System.Guid]$CorrelationId = [System.Guid]::NewGuid() | |
) | |
begin | |
{ | |
$logHeader = $null | |
} | |
process | |
{ | |
$logEntry = [PSCustomObject] @{ | |
Date = $(Get-Date).ToString("G") | |
TraceLevel = $TraceLevel | |
Message = $Message | |
CorrelationId = $CorrelationId | |
} | |
try | |
{ | |
if($LogPath) | |
{ | |
$mutex = New-Object System.Threading.Mutex($false, "Trace Log Mutex") | |
$mutexAcquired = $mutex.WaitOne(5000) # wait 5 seconds before timing out | |
if( $mutexAcquired ) | |
{ | |
if( -not (Test-Path -Path $LogPath -PathType Leaf) ) | |
{ | |
($logEntry | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[0] | Out-File -FilePath $LogPath -Append | |
} | |
# log the message to the log | |
($logEntry | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] | Out-File -FilePath $LogPath -Append | |
} | |
else | |
{ | |
Write-Error "Mutex could not be aquired." | |
$_ | |
} | |
} | |
switch( $TraceLevel ) | |
{ | |
"Critical" | |
{ | |
$global:CRITICAL_SCRIPT_FAILURE_OCCURRED = $true | |
Write-TraceLogEntry -Message "Flipped critical flag to true" -TraceLevel Verbose | |
Write-Host ($logEntry | SELECT Date, TraceLevel, Message | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] -ForegroundColor Red | |
} | |
"High" | |
{ | |
Write-Host ($logEntry | SELECT Date, TraceLevel, Message | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] -ForegroundColor Yellow | |
} | |
"Medium" | |
{ | |
Write-Host ($logEntry | SELECT Date, TraceLevel, Message | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] | |
} | |
"Low" | |
{ | |
Write-Host ($logEntry | SELECT Date, TraceLevel, Message | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] | |
} | |
"Verbose" | |
{ | |
# don't log verbose messages to the screen | |
# Write-Host ($logEntry | SELECT Date, TraceLevel, Message | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation)[1] | |
} | |
} | |
} | |
catch | |
{ | |
Write-Error "Unable to log message. Exception: $($_.Exception)" | |
} | |
finally | |
{ | |
if( $mutexAcquired -and $mutex ) | |
{ | |
$mutex.ReleaseMutex() | |
$mutex.Dispose() | |
} | |
} | |
} | |
end | |
{ | |
} | |
} | |
function Invoke-ClientContextWithRetry | |
{ | |
[cmdletbinding()] | |
param | |
( | |
[parameter(Mandatory=$true)][Microsoft.SharePoint.Client.ClientContext]$ClientContext, | |
[parameter(Mandatory=$false)][int]$Delay = 2, | |
[parameter(Mandatory=$false)][int]$RetryAttempts = 1 | |
) | |
begin | |
{ | |
$attemps = 1 | |
} | |
process | |
{ | |
do | |
{ | |
try | |
{ | |
Write-TraceLogEntry -Message "Executing client context query for $($ClientContext.Url)" -TraceLevel Verbose | |
$ClientContext.ExecuteQuery() | |
Write-TraceLogEntry -Message "Client context query executed" -TraceLevel Verbose | |
return | |
} | |
catch [System.Net.WebException] | |
{ | |
Write-TraceLogEntry -Message "Client context failed with a web exception." -TraceLevel Verbose | |
$response = $_.Exception.Response -as [System.Net.HttpWebResponse] | |
if( $response -and ( [int]$response.StatusCode -eq 429 -or [int]$response.StatusCode -eq 503 ) ) | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Execution attempt $retryAttempts failed" -TraceLevel High | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Exception Info: $($_.Exception)" -TraceLevel High | |
$retryAttempts++ | |
Start-Sleep -Seconds 10 | |
} | |
elseif( $response -and [int]$response.StatusCode -eq 404 ) | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - A site with the URL $($ClientContext.Url) was not found in the tenant." -TraceLevel High | |
return | |
} | |
} | |
catch | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Unexpected execution exception: $($_.Exception)" -TraceLevel High | |
throw $_.Exception | |
} | |
} | |
while( $retryAttempts -lt 5 ) | |
# we never had a successful execution | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Retry threshold ($RetryAttempts) exceeded." -TraceLevel High | |
throw [System.Exception] "$($MyInvocation.MyCommand.Name) - Retry attempts exceeded." | |
} | |
end | |
{ | |
} | |
} | |
function New-ClientContextWithRetry | |
{ | |
[cmdletbinding()] | |
param | |
( | |
[Parameter(Mandatory=$true, ParameterSetName = "CredentialComponents")] | |
[Parameter(Mandatory=$true, ParameterSetName = "SharePointOnlineCredential")] | |
[string]$ContextUrl, | |
[Parameter(Mandatory=$true, ParameterSetName = "CredentialComponents")] | |
[string]$UserName, | |
[Parameter(Mandatory=$true, ParameterSetName = "CredentialComponents")] | |
[System.Security.SecureString]$SecurePassword, | |
[Parameter(Mandatory=$true, ParameterSetName = "SharePointOnlineCredential")] | |
[Microsoft.SharePoint.Client.SharePointOnlineCredentials]$SharePointOnlineCredential | |
) | |
begin | |
{ | |
$retryAttempts = 1 | |
$clientContext = $null | |
} | |
process | |
{ | |
do | |
{ | |
try | |
{ | |
$clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($ContextUrl) | |
if( $PSCmdlet.ParameterSetName -eq "CredentialComponents" ) | |
{ | |
$clientContext.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $SecurePassword) | |
} | |
else | |
{ | |
$clientContext.Credentials = $SharePointOnlineCredential | |
} | |
$clientContext.Load($clientContext.Web) | |
$clientContext.Load($clientContext.Site) | |
$clientContext.ExecuteQuery() | |
return $clientContext | |
} | |
catch [System.Net.WebException] | |
{ | |
$response = $_.Exception.Response -as [System.Net.HttpWebResponse] | |
if( $response -and ( [int]$response.StatusCode -eq 429 -or [int]$response.StatusCode -eq 503 ) ) | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Execution attempt $retryAttempts failed" -TraceLevel High | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Exception Info: $($_.Exception)" -TraceLevel High | |
$retryAttempts++ | |
Start-Sleep -Seconds 10 | |
} | |
elseif( $response -and [int]$response.StatusCode -eq 404 ) | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - A site with the URL $ContextUrl was not found in the tenant." -TraceLevel High | |
return $null | |
} | |
else | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Unexpected execution exception: $($_.Exception)" -TraceLevel Critical | |
} | |
} | |
catch | |
{ | |
Write-TraceLogEntry -Message "$($MyInvocation.MyCommand.Name) - Unexpected execution exception: $($_.Exception)" -TraceLevel Critical | |
throw $_.Exception | |
} | |
} | |
while( $retryAttempts -lt 5 ) | |
} | |
end | |
{ | |
} | |
} | |
function Add-DomainGroup | |
{ | |
[CmdletBinding()] | |
param | |
( | |
[parameter(Mandatory=$true)][Microsoft.SharePoint.Client.ClientContext]$ClientContext, | |
[Parameter(Mandatory=$true)][string]$GroupName, | |
[Parameter(Mandatory=$true)][string[]]$Permissions | |
) | |
begin | |
{ | |
Write-TraceLogEntry -Message "Starting $($ClientContext.Url)" -TraceLevel Low | |
} | |
process | |
{ | |
# pull a list of all the site users | |
$siteUsers = $ClientContext.Web.SiteUsers | |
$ClientContext.Load( $siteUsers ) | |
Invoke-ClientContextWithRetry -ClientContext $ClientContext | |
# search the site for the existing domain group | |
#$domainGroup = $siteUsers | ? { $_.PrincipalType -eq "SecurityGroup" -and $_.Email -match "$GroupName@" } | |
$domainGroup = $siteUsers | ? { $_.PrincipalType -eq "SecurityGroup" -and $_.Title -eq $GroupName } | |
# target domain group was not found on the site | |
if( -not $domainGroup ) | |
{ | |
Write-TraceLogEntry -Message "Querying SharePoint for '$GroupName'" -TraceLevel Verbose | |
# query for the group by name | |
$principalSearchResults = [Microsoft.SharePoint.Client.Utilities.Utility]::SearchPrincipals( | |
$ClientContext, | |
$clientContext.Web, | |
$GroupName, | |
[Microsoft.SharePoint.Client.Utilities.PrincipalType]::SecurityGroup, | |
[Microsoft.SharePoint.Client.Utilities.PrincipalSource]::All, | |
$null <# users container #>, | |
1 <# max results #>) | |
Invoke-ClientContextWithRetry -ClientContext $ClientContext | |
Write-TraceLogEntry -Message "Principal query found $($principalSearchResults.Count) results." -TraceLevel Verbose | |
if( $principalSearchResults.Count -eq 1 ) | |
{ | |
Write-TraceLogEntry -Message "Calling EnsureUser for $($principalSearchResults[0].LoginName)" -TraceLevel Verbose | |
$domainGroup = $ClientContext.Web.EnsureUser( $principalSearchResults[0].LoginName ) | |
$domainGroup.Update() | |
$ClientContext.Web.Update() | |
$ClientContext.Load($domainGroup) | |
Invoke-ClientContextWithRetry -ClientContext $ClientContext | |
} | |
} | |
else | |
{ | |
Write-TraceLogEntry -Message "Group '$GroupName' was found in the User Information List." -TraceLevel Verbose | |
} | |
# domain group was already on the site or it was just added to the site | |
if( $domainGroup ) | |
{ | |
Write-TraceLogEntry -Message "Adding group '$($domainGroup.Title)' to site permissions." -TraceLevel Low | |
foreach( $permission in $Permissions ) | |
{ | |
Write-TraceLogEntry -Message "Attempting to grant '$($domainGroup.Title)' the '$Permission' permission" -TraceLevel Verbose | |
try | |
{ | |
$roleDefinition = $ClientContext.Site.RootWeb.RoleDefinitions.GetByName( $permission ) | |
if( $roleDefinition ) | |
{ | |
$roleAssignment = New-Object Microsoft.SharePoint.Client.RoleDefinitionBindingCollection( $ClientContext ) | |
$roleAssignment.Add( $roleDefinition ) | |
$roleAssigment = $ClientContext.Site.RootWeb.RoleAssignments.Add( $domainGroup, $roleAssignment ) | |
$ClientContext.Load( $roleAssignment ) | |
Invoke-ClientContextWithRetry -ClientContext $ClientContext | |
Write-TraceLogEntry -Message "Granted $($domainGroup.Title) '$Permission' permission" -TraceLevel Verbose | |
} | |
else | |
{ | |
Write-TraceLogEntry -Message "Permission not found: '$permission'" -TraceLevel High | |
} | |
} | |
catch | |
{ | |
Write-TraceLogEntry -Message "Failed to apply '$Permission' to group '$($domainGroup.Title)'. Exception: $_" -TraceLevel High | |
} | |
} | |
} | |
else | |
{ | |
Write-TraceLogEntry -Message "Domain Group '$GroupName' was not found in Azure AD." -TraceLevel Critical | |
} | |
} | |
end | |
{ | |
Write-TraceLogEntry -Message "Ending $($ClientContext.Url)" -TraceLevel Low | |
} | |
} | |
Write-TraceLogEntry -Message "Reading input file from $($inputFile)" -TraceLevel Low | |
$missingGroups = Import-Csv -Path $inputFile | |
$counter = 1 | |
foreach( $missingGroup in $missingGroups ) | |
{ | |
Write-TraceLogEntry -Message "Processing Site: $($missingGroup.TargetUrl)" -TraceLevel Medium | |
# read the data | |
$targetUrl = $missingGroup.TargetUrl | |
$groupName = $missingGroup.GroupOrUserName | |
$permission = $missingGroup.SourcePermission -split "," | |
$permission = New-Object System.Collections.ArrayList(,$permission) | |
# remove some invalid perms | |
foreach( $permLevel in @("Limited Access") ) | |
{ | |
$permission.Remove( $permLevel ) | |
} | |
if( $permission.Count -eq 0 ) | |
{ | |
Write-TraceLogEntry -Message "There are no eligible permission levels to add to $targetUrl for row $counter in the input file." -TraceLevel High | |
continue | |
} | |
# remove some of the known SharePoint groups | |
if( @( "Excel Services Viewers", "Approvers", "Designers", "Conversion Group", "Style Resource Readers", "Restricated Readers") -contains $groupName ) | |
{ | |
Write-TraceLogEntry -Message "The SharePoint group '$groupName' is not a domain group and will not be processed." -TraceLevel High | |
continue | |
} | |
# attempt to create a client context for the site | |
$clientContext = New-ClientContextWithRetry -ContextUrl $targetUrl -UserName $credential.UserName -SecurePassword $credential.Password | |
# process the group adds for the site | |
if( $clientContext.TraceCorrelationId ) | |
{ | |
Write-TraceLogEntry -Message "Adding domain groups to $targetUrl" -TraceLevel Medium | |
Add-DomainGroup -ClientContext $clientContext -GroupName $groupName -Permissions $permission | |
} | |
else | |
{ | |
Write-TraceLogEntry -Message "Failed to create a client context for $($targetUrl)" -TraceLevel High | |
} | |
$counter++ | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment