Skip to content

Instantly share code, notes, and snippets.

@joerodgers
Last active January 2, 2018 13:49
Show Gist options
  • Save joerodgers/c72c8bcc969866d09278003ba33dc819 to your computer and use it in GitHub Desktop.
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)
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