博客地址:http://blog.csdn.net/FoxDave
本文阐述通过编程方式连接到Office 365组的第二部分。
第三步:现代化我们的网站
批量连接组的过程包括两个步骤:
- 准备并验证一个用来执行批量组连接过程的输入文件
- 执行批量组连接过程
为批量组连接和验证创建一个输入文件
运行扫描器得到结果之后,我们就知道了哪些网站可以进行组连接。下一步就是准备一个CSV文件来执行批量组连接过程。CSV文件格式很简单:
- URL列包含要进行组连接的网站集的URL。
- Alias包含了我们想要使用的Office 365组的名称。注意不能包含空格并且之前没有被使用过。
- IsPublic表示我们想让网站是公共的还是私有的。
- Classification表示我们在进行组连接之后希望网站是哪个类别。我们需要设置它是因为将网站连接到一个组之后,类别就是在Office 365组级别进行维护了。
下面时一个简单的示例:
Url,Alias,IsPublic,Classification
https://contoso.sharepoint.com/sites/hrteam,hrteam,false,Medium Impact
https://contoso.sharepoint.com/sites/engineering,engineeringteam,true,Low Impact
为了帮助我们在使用文件之前进行验证,我们可以使用本部分结尾的PowerShell脚本。这个脚本会检查网站URL和组的名称。打开脚本将里面的管理中心URL更新成我们自己的然后执行它。脚本需要我们填写CSV文件的名称并在执行后生成一个报告。
在脚本执行过程中,可能会出现以下错误:
-
[ERROR] AzureAD Naming policy : PrefixSuffix does contain AD attributes that are resolved based on the user running the group-connection
在Azure AD中,我们可以定义Office 365组的命名策略。如果策略包含了用户的AD属性,可能会造成问题,因为批量组连接操作时所有的网站都会使用当前用户。 -
[ERROR] AzureAD Creation policy : adminUPN is not part of group CanCreateGroupsId that controls Office 365 group creation
如果Azure AD的组被限制为只有指定用户能够创建并且当前用户不具有权限的化,会发生失败。 -
[ERROR] siteUrl : Alias [siteAlias] contains a space, which is not allowed
Office 365组的别名不能包含空格。 -
[ERROR] siteUrl : Classification [siteClassification] does not comply with available Azure AD classifications [ClassificationListString]
提供的网站分类没有在Office组允许的网站分类中定义。 -
[ERROR] siteUrl : Alias [siteAlias] is in the Azure AD blocked word list [CustomBlockedWordsListString]
如果在Azure AD中设置了屏蔽的单词列表并且提供的Office 365组名使用了其中的词就会发生这个错误。 -
[ERROR] siteUrl : Site is already connected to a group
一个网站只能连接到单一的Office 365组,也就是说连接后的网站不能再次连接了。 -
[ERROR] siteUrl : Alias [siteAlias] is already in use
每个Office 365组都需要一个唯一的别名,如果别名已经存在了就会发生这个错误。 -
[ERROR] siteUrl : Alias [siteAlias] was already marked as approved alias for another site in this file
提供的网站别名已经在CSV文件中前面的部分被定义过了。 -
[ERROR] siteUrl : Site does not exist or is not available (status = site.Status)
提供的网站URL不是可访问的网站集。
注意:将下面脚本中的变量$tenantAdminUrl更新为我们自己的租户管理中心URL。
在脚本执行过程中,会生成一个log文件,加上一个只包含错误的log文件的子集文件。
#region Logging and generic functions
function LogWrite
{
param([string] $log , [string] $ForegroundColor)
$global:strmWrtLog.writeLine($log)
if([string]::IsNullOrEmpty($ForegroundColor))
{
Write-Host $log
}
else
{
Write-Host $log -ForegroundColor $ForegroundColor
}
}
function LogError
{
param([string] $log)
$global:strmWrtError.writeLine($log)
}
function IsGuid
{
param([string] $owner)
try
{
[GUID]$g = $owner
$t = $g.GetType()
return ($t.Name -eq "Guid")
}
catch
{
return $false
}
}
function IsGroupConnected
{
param([string] $owner)
if (-not [string]::IsNullOrEmpty($owner))
{
if ($owner.Length -eq 38)
{
if ((IsGuid $owner.Substring(0, 36)) -and ($owner.Substring(36, 2) -eq "_o"))
{
return $true
}
}
}
return $false
}
function ContainsADAttribute
{
param($PrefixSuffix)
$ADAttributes = @("[Department]", "[Company]", "[Office]", "[StateOrProvince]", "[CountryOrRegion]", "[Title]")
foreach($attribute in $ADAttributes)
{
if ($PrefixSuffix -like "*$attribute*")
{
return $true
}
}
return $false
}
#endregion
#######################################################
# MAIN section #
#######################################################
# Tenant admin url
$tenantAdminUrl = "https://contoso-admin.sharepoint.com"
# If you use credential manager then specify the used credential manager entry, if left blank you'll be asked for a user/pwd
$credentialManagerCredentialToUse = ""
#region Setup Logging
$date = Get-Date
$logfile = ((Get-Item -Path ".\" -Verbose).FullName + "\GroupifyInputValidation_log_" + $date.ToFileTime() + ".txt")
$global:strmWrtLog=[System.IO.StreamWriter]$logfile
$global:Errorfile = ((Get-Item -Path ".\" -Verbose).FullName + "\GroupifyInputValidation_error_" + $date.ToFileTime() + ".txt")
$global:strmWrtError=[System.IO.StreamWriter]$Errorfile
#endregion
#region Load needed PowerShell modules
#Ensure PnP PowerShell is loaded
$minimumVersion = New-Object System.Version("2.24.1803.0")
if (-not (Get-InstalledModule -Name SharePointPnPPowerShellOnline -MinimumVersion $minimumVersion -ErrorAction Ignore))
{
Install-Module SharePointPnPPowerShellOnline -MinimumVersion $minimumVersion -Scope CurrentUser -Force
}
Import-Module SharePointPnPPowerShellOnline -DisableNameChecking -MinimumVersion $minimumVersion
#endregion
#region Ensure Azure PowerShell is loaded
$minimumAzurePowerShellVersion = New-Object System.Version("2.0.0.137")
if (-not (Get-InstalledModule -Name AzureADPreview -MinimumVersion $minimumAzurePowerShellVersion -ErrorAction Ignore))
{
install-module AzureADPreview -MinimumVersion $minimumAzurePowerShellVersion -Scope CurrentUser -Force
}
Import-Module AzureADPreview -MinimumVersion $minimumAzurePowerShellVersion
$siteURLFile = Read-Host -Prompt 'Input name of .CSV file to validate (e.g. sitecollections.csv) ?'
# Get the tenant admin credentials.
$credentials = $null
$adminUPN = $null
if(![String]::IsNullOrEmpty($credentialManagerCredentialToUse) -and (Get-PnPStoredCredential -Name $credentialManagerCredentialToUse) -ne $null)
{
$adminUPN = (Get-PnPStoredCredential -Name $credentialManagerCredentialToUse).UserName
$credentials = $credentialManagerCredentialToUse
$azureADCredentials = Get-PnPStoredCredential -Name $credentialManagerCredentialToUse -Type PSCredential
}
else
{
# Prompts for credentials, if not found in the Windows Credential Manager.
$adminUPN = Read-Host -Prompt "Please enter admin UPN"
$pass = Read-host -AsSecureString "Please enter admin password"
$credentials = new-object management.automation.pscredential $adminUPN,$pass
$azureADCredentials = $credentials
}
if($credentials -eq $null)
{
Write-Host "Error: No credentials supplied." -ForegroundColor Red
exit 1
}
#endregion
#region Connect to SharePoint and Azure
# Get a tenant admin connection, will be reused in the remainder of the script
LogWrite "Connect to tenant admin site $tenantAdminUrl"
$tenantContext = Connect-PnPOnline -Url $tenantAdminUrl -Credentials $credentials -Verbose -ReturnConnection
LogWrite "Connect to Azure AD"
$azureUser = Connect-AzureAD -Credential $azureADCredentials
#endregion
#region Read Azure AD group settings
$groupSettings = (Get-AzureADDirectorySetting | Where-Object -Property DisplayName -Value "Group.Unified" -EQ)
$CheckGroupCreation = $false
$CanCreateGroupsId = $null
$CheckClassificationList = $false
$ClassificationList = $null
$CheckPrefixSuffix = $false
$PrefixSuffix = $null
$CheckDefaultClassification = $false
$DefaultClassification = $null
$CheckCustomBlockedWordsList = $false
if (-not ($groupSettings -eq $null))
{
if (-not($groupSettings["EnableGroupCreation"] -eq $true))
{
# Group creation is restricted to a security group...verify if the current user is part of that group
# See: https://support.office.com/en-us/article/manage-who-can-create-office-365-groups-4c46c8cb-17d0-44b5-9776-005fced8e618?ui=en-US&rs=en-001&ad=US
$CheckGroupCreation = $true
$CanCreateGroupsId = $groupSettings["GroupCreationAllowedGroupId"]
}
if (-not ($groupSettings["CustomBlockedWordsList"] -eq ""))
{
# Check for blocked words in group name
# See: https://support.office.com/en-us/article/office-365-groups-naming-policy-6ceca4d3-cad1-4532-9f0f-d469dfbbb552?ui=en-US&rs=en-001&ad=US
$CheckCustomBlockedWordsList = $true
$option = [System.StringSplitOptions]::RemoveEmptyEntries
$CustomBlockedWordsListString = $groupSettings["CustomBlockedWordsList"]
$CustomBlockedWordsList = $groupSettings["CustomBlockedWordsList"].Split(",", $option)
# Trim array elements
[int] $arraycounter = 0
foreach($c in $CustomBlockedWordsList)
{
$CustomBlockedWordsList[$arraycounter] = $c.Trim(" ")
$arraycounter++
}
}
if (-not ($groupSettings["PrefixSuffixNamingRequirement"] -eq ""))
{
# Check for prefix/suffix naming - any dynamic tokens beside [groupname] can be problematic since all
# groups are created using the user running the bulk groupify
# See: https://support.office.com/en-us/article/office-365-groups-naming-policy-6ceca4d3-cad1-4532-9f0f-d469dfbbb552?ui=en-US&rs=en-001&ad=US
$CheckPrefixSuffix = $true
$PrefixSuffix = $groupSettings["PrefixSuffixNamingRequirement"]
}
if (-not ($groupSettings["ClassificationList"] -eq ""))
{
# Check for valid classification labels
# See: https://support.office.com/en-us/article/Manage-Office-365-Groups-with-PowerShell-aeb669aa-1770-4537-9de2-a82ac11b0540
$CheckClassificationList = $true
$option = [System.StringSplitOptions]::RemoveEmptyEntries
$ClassificationListString = $groupSettings["ClassificationList"]
$ClassificationList = $groupSettings["ClassificationList"].Split(",", $option)
# Trim array elements
[int] $arraycounter = 0
foreach($c in $ClassificationList)
{
$ClassificationList[$arraycounter] = $c.Trim(" ")
$arraycounter++
}
if (-not ($groupSettings["DefaultClassification"] -eq ""))
{
$CheckDefaultClassification = $true
$DefaultClassification = $groupSettings["DefaultClassification"].Trim(" ")
}
}
}
#endregion
#region Validate input
LogWrite "General Azure AD validation"
if ($CheckPrefixSuffix -and (ContainsADAttribute $PrefixSuffix))
{
$message = "[ERROR] AzureAD Naming policy : $PrefixSuffix does contain AD attributes that are resolved based on the user running the groupify"
LogWrite $message Red
LogError $message
}
if ($CheckGroupCreation)
{
$groupToCheck = new-object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck
$groupToCheck.GroupIds = $CanCreateGroupsId
$accountToCheck = Get-AzureADUser -SearchString $adminUPN
$groupsUserIsMemberOf = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $accountToCheck.ObjectId -GroupIdsForMembershipCheck $groupToCheck
if ($groupsUserIsMemberOf -eq $null)
{
$message = "[ERROR] AzureAD Creation policy : $adminUPN is not part of group $CanCreateGroupsId which controls Office 365 Group creation"
LogWrite $message Red
LogError $message
}
}
# "approved" aliases
$approvedAliases = @{}
LogWrite "Validating rows in $siteURLFile..."
$csvRows = Import-Csv $siteURLFile
foreach($row in $csvRows)
{
if($row.Url.Trim() -ne "")
{
$siteUrl = $row.Url
$siteAlias = $row.Alias
$siteClassification = $row.Classification
if ($siteClassification -ne $null)
{
$siteClassification = $siteClassification.Trim(" ")
}
LogWrite "[VALIDATING] $siteUrl with alias [$siteAlias] and classification [$siteClassification]"
try
{
# First perform validations that do not require to load the site
if ($siteAlias.IndexOf(" ") -gt 0)
{
$message = "[ERROR] $siteUrl : Alias [$siteAlias] contains a space, which not allowed"
LogWrite $message Red
LogError $message
}
elseif (($CheckClassificationList -eq $true) -and (-not ($ClassificationList -contains $siteClassification)))
{
$message = "[ERROR] $siteUrl : Classification [$siteClassification] does not comply with available AzureAD classifications [$ClassificationListString]"
LogWrite $message Red
LogError $message
}
elseif (($CheckCustomBlockedWordsList -eq $true) -and ($CustomBlockedWordsList -contains $siteAlias))
{
$message = "[ERROR] $siteUrl : Alias [$siteAlias] is in the AzureAD blocked word list [$CustomBlockedWordsListString]"
LogWrite $message Red
LogError $message
}
else
{
# try getting the site
$site = Get-PnPTenantSite -Url $siteUrl -Connection $tenantContext -ErrorAction Ignore
if ($site.Status -eq "Active")
{
if (IsGroupConnected $site.Owner)
{
$message = "[ERROR] $siteUrl : Site is already connected a group"
LogWrite $message Red
LogError $message
}
else
{
$aliasIsUsed = Test-PnPOffice365GroupAliasIsUsed -Alias $siteAlias -Connection $tenantContext
if ($aliasIsUsed)
{
$message = "[ERROR] $siteUrl : Alias [$siteAlias] is already in use"
LogWrite $message Red
LogError $message
}
elseif ($approvedAliases.ContainsKey($siteAlias))
{
$message = "[ERROR] $siteUrl : Alias [$siteAlias] was already marked as approved alias for another site in this file"
LogWrite $message Red
LogError $message
}
else
{
$approvedAliases.Add($siteAlias, $siteAlias)
LogWrite "[VALIDATED] $siteUrl with alias [$siteAlias] and classification [$siteClassification]" Green
}
}
}
else
{
$message = "[ERROR] $siteUrl : Site does not exist or is not available (status = $($site.Status))"
LogWrite $message Red
LogError $message
}
}
}
catch [Exception]
{
$ErrorMessage = $_.Exception.Message
LogWrite "Error: $ErrorMessage" Red
LogError $ErrorMessage
}
}
}
#endregion
#region Close log files
if ($global:strmWrtLog -ne $NULL)
{
$global:strmWrtLog.Close()
$global:strmWrtLog.Dispose()
}
if ($global:strmWrtError -ne $NULL)
{
$global:strmWrtError.Close()
$global:strmWrtError.Dispose()
}
#endregion
执行批量组连接过程
现在我们有了定义需要执行组连接操作的网站的输入文件,我们终于可以去执行了。下面的PowerShell脚本是一个示例,我们使用的时候需要做一些微调,因为相关的具体设置需求每个人可能是不一样的。
示例脚本执行了以下步骤:
- 在必要时添加当前租户管理员作为网站管理员;组连接需要一个用户账户。
- 验证网站模板/发布功能是否使用而影响组连接;跟扫描工具中的逻辑保持一致。
- 确保没有阻碍现代化的功能被启用,如果有就修复一下。
- 确保现代化页面功能时启用的。
- 可选的:部署应用程序(例如,应用程序自定义)
- 可选的:添加我们自己的现代化主页。
- 调用组连接API。
- 定义网站管理员和网站所有者作为组的所有者。
- 定义网站成员作为组成员。
- 从SharePoint管理中心移除添加的租户管理员和网站所有者。
- 从Office 365组移除添加的租户管理员。
执行下面的PowerShell脚本需要我们更新租户管理中心的URL并且在运行时提供凭据和CSV输入文件。
注意:我们可以根据需要去更新/移除可选部分的脚本,或添加额外的任务现代化任务(如设置网站的主题)。同样地,还需要将$tenantAdminUrl更改为自己的。
#region Logging and generic functions
function LogWrite
{
param([string] $log , [string] $ForegroundColor)
$global:strmWrtLog.writeLine($log)
if([string]::IsNullOrEmpty($ForegroundColor))
{
Write-Host $log
}
else
{
Write-Host $log -ForegroundColor $ForegroundColor
}
}
function LogError
{
param([string] $log)
$global:strmWrtError.writeLine($log)
}
function LoginNameToUPN
{
param([string] $loginName)
return $loginName.Replace("i:0#.f|membership|", "")
}
function AddToOffice365GroupOwnersMembers
{
param($groupUserUpn, $groupId, [bool] $Owners)
# Apply an incremental backoff strategy as after group creation the group is not immediately available on all Azure AD nodes resulting in resource not found errors
# It can take up to a minute to get all Azure AD nodes in sync
$retryCount = 5
$retryAttempts = 0
$backOffInterval = 2
LogWrite "Attempting to add $groupUserUpn to group $groupId"
while($retryAttempts -le $retryCount)
{
try
{
if ($Owners)
{
$azureUserId = Get-AzureADUser -ObjectId $groupUserUpn
Add-AzureADGroupOwner -ObjectId $groupId -RefObjectId $azureUserId.ObjectId
LogWrite "User $groupUserUpn added as group owner"
}
else
{
$azureUserId = Get-AzureADUser -ObjectId $groupUserUpn
Add-AzureADGroupMember -ObjectId $groupId -RefObjectId $azureUserId.ObjectId
LogWrite "User $groupUserUpn added as group member"
}
$retryAttempts = $retryCount + 1;
}
catch
{
if ($retryAttempts -lt $retryCount)
{
$retryAttempts = $retryAttempts + 1
Write-Host "Retry attempt number: $retryAttempts. Sleeping for $backOffInterval seconds..."
Start-Sleep $backOffInterval
$backOffInterval = $backOffInterval * 2
}
else
{
throw
}
}
}
}
function UsageLog
{
try
{
$cc = Get-PnPContext
$cc.Load($cc.Web)
$cc.ClientTag = "SPDev:GroupifyPS"
$cc.ExecuteQuery()
}
catch [Exception] { }
}
#endregion
function GroupifySite
{
param([string] $siteCollectionUrl,
[string] $alias,
[Boolean] $isPublic,
[string] $siteClassification,
$credentials,
$tenantContext,
[string] $adminUPN)
#region Ensure access to the site collection, if needed promote the calling account to site collection admin
# Check if we can access the site...if not let's 'promote' ourselves as site admin
$adminClaim = "i:0#.f|membership|$adminUPN"
$adminWasAdded = $false
$siteOwnersGroup = $null
$siteContext = $null
$siteCollectionUrl = $siteCollectionUrl.TrimEnd("/");
Try
{
LogWrite "User running groupify: $adminUPN"
LogWrite "Connecting to site $siteCollectionUrl"
$siteContext = Connect-PnPOnline -Url $siteCollectionUrl -Credentials $credentials -Verbose -ReturnConnection
}
Catch [Exception]
{
# If Access Denied then use tenant API to add current tenant admin user as site collection admin to the current site
if ($_.Exception.Response.StatusCode -eq "Unauthorized")
{
LogWrite "Temporarily adding user $adminUPN as site collection admin"
Set-PnPTenantSite -Url $siteCollectionUrl -Owners @($adminUPN) -Connection $tenantContext
$adminWasAdded = $true
LogWrite "Second attempt to connect to site $siteCollectionUrl"
$siteContext = Connect-PnPOnline -Url $siteCollectionUrl -Credentials $credentials -Verbose -ReturnConnection
}
else
{
$ErrorMessage = $_.Exception.Message
LogWrite "Error for site $siteCollectionUrl : $ErrorMessage" Red
LogError $ErrorMessage
return
}
}
#endregion
Try
{
# Groupify steps
# - [Done] Add current tenant admin as site admin when needed
# - [Done] Verify site template / publishing feature use and prevent groupify --> align with the logic in the scanner
# - [Done] Ensure no modern blocking features are enabled...if so fix it
# - [Done] Ensure the modern page feature is enabled
# - [Done] Optional: Deploy applications (e.g. application customizer)
# - [Done] Optional: Add modern home page
# - [Done] Call groupify API
# - [Done] Define Site Admins and Site owners as group owners
# - [Done] Define Site members as group members
# - [] Have option to "expand" site owners/members if needed
# - [Done] Remove added tenant admin and site owners from SharePoint admins
# - [Done] Remove added tenant admin from the Office 365 group
#region Adding admin
# Check if current tenant admin is part of the site collection admins, if not add the account
$siteAdmins = $null
if ($adminWasAdded -eq $false)
{
try
{
# Eat exceptions here...resulting $siteAdmins variable will be empty which will trigger the needed actions
$siteAdmins = Get-PnPSiteCollectionAdmin -Connection $siteContext -ErrorAction Ignore
}
catch [Exception] { }
$adminNeedToBeAdded = $true
foreach($admin in $siteAdmins)
{
if ($admin.LoginName -eq $adminClaim)
{
$adminNeedToBeAdded = $false
break
}
}
if ($adminNeedToBeAdded)
{
LogWrite "Temporarily adding user $adminUPN as site collection admin"
Set-PnPTenantSite -Url $siteCollectionUrl -Owners @($adminUPN) -Connection $tenantContext
$adminWasAdded = $true
}
}
UsageLog
#endregion
#region Checking for "blockers"
$publishingSiteFeature = Get-PnPFeature -Identity "F6924D36-2FA8-4F0B-B16D-06B7250180FA" -Scope Site -Connection $siteContext
$publishingWebFeature = Get-PnPFeature -Identity "94C94CA6-B32F-4DA9-A9E3-1F3D343D7ECB" -Scope Web -Connection $siteContext
if (($publishingSiteFeature.DefinitionId -ne $null) -or ($publishingWebFeature.DefinitionId -ne $null))
{
throw "Publishing feature enabled...can't groupify this site"
}
# Grab the web template and verify if it's a groupify blocker
$web = Get-PnPWeb -Connection $siteContext -Includes WebTemplate,Configuration,Description
$webTemplate = $web.WebTemplate + $web.Configuration
if ($webTemplate -eq "BICENTERSITE#0" -or
$webTemplate -eq "BLANKINTERNET#0" -or
$webTemplate -eq "ENTERWIKI#0" -or
$webTemplate -eq "SRCHCEN#0" -or
$webTemplate -eq "SRCHCENTERLITE#0" -or
$webTemplate -eq "POINTPUBLISHINGHUB#0" -or
$webTemplate -eq "POINTPUBLISHINGTOPIC#0" -or
$siteCollectionUrl.EndsWith("/sites/contenttypehub"))
{
throw "Incompatible web template detected...can't groupify this site"
}
#endregion
#region Enable full modern experience by enabling the pages features and disabling "blocking" features
LogWrite "Enabling modern page feature, disabling modern list UI blocking features"
# Enable modern page feature
Enable-PnPFeature -Identity "B6917CB1-93A0-4B97-A84D-7CF49975D4EC" -Scope Web -Force -Connection $siteContext
# Disable the modern list site level blocking feature
Disable-PnPFeature -Identity "E3540C7D-6BEA-403C-A224-1A12EAFEE4C4" -Scope Site -Force -Connection $siteContext
# Disable the modern list web level blocking feature
Disable-PnPFeature -Identity "52E14B6F-B1BB-4969-B89B-C4FAA56745EF" -Scope Web -Force -Connection $siteContext
#endregion
#region Optional: Add SharePoint Framework customizations - sample
LogWrite "Deploying SPFX application customizer"
Add-PnPCustomAction -Name "Footer" -Title "Footer" -Location "ClientSideExtension.ApplicationCustomizer" -ClientSideComponentId "edbe7925-a83b-4d61-aabf-81219fdc1539" -ClientSideComponentProperties "{}"
#endregion
#region Optional: Add custom home page - sample
LogWrite "Deploying a custom modern home page"
$homePage = Get-PnPHomePage -Connection $siteContext
$newHomePageName = $homePage.Substring($homePage.IndexOf("/") + 1).Replace(".aspx", "_new.aspx")
$newHomePagePath = $homePage.Substring(0, $homePage.IndexOf("/") + 1)
$newHomePage = Add-PnPClientSidePage -Name $newHomePageName -LayoutType Article -CommentsEnabled:$false -Publish:$true -Connection $siteContext
# Add your additional web parts here!
Add-PnPClientSidePageSection -Page $newHomePage -SectionTemplate OneColumn -Order 1 -Connection $siteContext
Add-PnPClientSideText -Page $newHomePage -Text "Old home page was <a href=""$siteCollectionUrl/$homePage"">here</a>" -Section 1 -Column 1
Set-PnPHomePage -RootFolderRelativeUrl ($newHomePagePath + $newHomePageName) -Connection $siteContext
#endregion
#region Prepare for group permission configuration
# Get admins again now that we've ensured our access
$siteAdmins = Get-PnPSiteCollectionAdmin -Connection $siteContext
# Get owners and members before the group claim gets added
$siteOwnersGroup = Get-PnPGroup -AssociatedOwnerGroup -Connection $siteContext
$siteMembersGroup = Get-PnPGroup -AssociatedMemberGroup -Connection $siteContext
#endregion
#region Call groupify API
LogWrite "Call groupify API with following settings: Alias=$alias, IsPublic=$isPublic, Classification=$siteClassification"
Add-PnPOffice365GroupToSite -Url $siteCollectionUrl -Alias $alias -DisplayName $alias -Description $web.Description -IsPublic:$isPublic -KeepOldHomePage:$true -Classification $siteClassification -Connection $siteContext
#endregion
#region Configure group permissions
LogWrite "Adding site administrators and site owners to the Office 365 group owners"
$groupOwners = @{}
foreach($siteAdmin in $siteAdmins)
{
if (($siteAdmin.LoginName).StartsWith("i:0#.f|membership|"))
{
$siteAdminUPN = (LoginNameToUPN $siteAdmin.LoginName)
if (-not ($siteAdminUPN -eq $adminUPN))
{
if (-not ($groupOwners.ContainsKey($siteAdminUPN)))
{
$groupOwners.Add($siteAdminUPN, $siteAdminUPN)
}
}
}
else
{
#TODO: group expansion?
}
}
foreach($siteOwner in $siteOwnersGroup.Users)
{
if (($siteOwner.LoginName).StartsWith("i:0#.f|membership|"))
{
$siteOwnerUPN = (LoginNameToUPN $siteOwner.LoginName)
if (-not ($groupOwners.ContainsKey($siteOwnerUPN)))
{
$groupOwners.Add($siteOwnerUPN, $siteOwnerUPN)
}
}
else
{
#TODO: group expansion?
}
}
$site = Get-PnPSite -Includes GroupId -Connection $siteContext
foreach($groupOwner in $groupOwners.keys)
{
try
{
AddToOffice365GroupOwnersMembers $groupOwner ($site.GroupId) $true
}
catch [Exception]
{
$ErrorMessage = $_.Exception.Message
LogWrite "Error adding user $groupOwner to group owners. Error: $ErrorMessage" Red
LogError $ErrorMessage
}
}
LogWrite "Adding site members to the Office 365 group members"
$groupMembers = @{}
foreach($siteMember in $siteMembersGroup.Users)
{
if (($siteMember.LoginName).StartsWith("i:0#.f|membership|"))
{
$siteMemberUPN = (LoginNameToUPN $siteMember.LoginName)
if (-not ($groupMembers.ContainsKey($siteMemberUPN)))
{
$groupMembers.Add($siteMemberUPN, $siteMemberUPN)
}
}
else
{
#TODO: group expansion?
}
}
foreach($groupMember in $groupMembers.keys)
{
try
{
AddToOffice365GroupOwnersMembers $groupMember ($site.GroupId) $false
}
catch [Exception]
{
$ErrorMessage = $_.Exception.Message
LogWrite "Error adding user $groupMember to group members. Error: $ErrorMessage" Red
LogError $ErrorMessage
}
}
#endregion
#region Cleanup updated permissions
LogWrite "Groupify is done, let's cleanup the configured permissions"
# Remove the added site collection admin - obviously this needs to be the final step in the script :-)
if ($adminWasAdded)
{
#Remove the added site admin from the Office 365 Group owners and members
LogWrite "Remove $adminUPN from the Office 365 group owners and members"
$site = Get-PnPSite -Includes GroupId -Connection $siteContext
$azureAddedAdminId = Get-AzureADUser -ObjectId $adminUPN
try
{
Remove-AzureADGroupOwner -ObjectId $site.GroupId -OwnerId $azureAddedAdminId.ObjectId -ErrorAction Ignore
Remove-AzureADGroupMember -ObjectId $site.GroupId -MemberId $azureAddedAdminId.ObjectId -ErrorAction Ignore
}
catch [Exception] { }
LogWrite "Remove $adminUPN from site collection administrators"
Remove-PnPSiteCollectionAdmin -Owners @($adminUPN) -Connection $siteContext
}
#endregion
LogWrite "Groupify done for site collection $siteCollectionUrl" Green
# Disconnect PnP Powershell from site
Disconnect-PnPOnline
}
Catch [Exception]
{
$ErrorMessage = $_.Exception.Message
LogWrite "Error: $ErrorMessage" Red
LogError $ErrorMessage
#region Cleanup updated permissions on error
# Groupify run did not complete...remove the added tenant admin to restore site permissions as final step in the cleanup
if ($adminWasAdded)
{
# Below logic might fail if the error happened before the groupify API call, but errors are ignored
$site = Get-PnPSite -Includes GroupId -Connection $siteContext
$azureAddedAdminId = Get-AzureADUser -ObjectId $adminUPN
try
{
Remove-AzureADGroupOwner -ObjectId $site.GroupId -OwnerId $azureAddedAdminId.ObjectId -ErrorAction Ignore
Remove-AzureADGroupMember -ObjectId $site.GroupId -MemberId $azureAddedAdminId.ObjectId -ErrorAction Ignore
# Final step, remove the added site collection admin
Remove-PnPSiteCollectionAdmin -Owners @($adminUPN) -Connection $siteContext
}
catch [Exception] { }
}
#endregion
LogWrite "Groupify failed for site collection $siteCollectionUrl" Red
}
}
#######################################################
# MAIN section #
#######################################################
# Tenant admin url
$tenantAdminUrl = "https://contoso-admin.sharepoint.com"
# If you use credential manager then specify the used credential manager entry, if left blank you'll be asked for a user/pwd
$credentialManagerCredentialToUse = ""
#region Setup Logging
$date = Get-Date
$logfile = ((Get-Item -Path ".\" -Verbose).FullName + "\Groupify_log_" + $date.ToFileTime() + ".txt")
$global:strmWrtLog=[System.IO.StreamWriter]$logfile
$global:Errorfile = ((Get-Item -Path ".\" -Verbose).FullName + "\Groupify_error_" + $date.ToFileTime() + ".txt")
$global:strmWrtError=[System.IO.StreamWriter]$Errorfile
#endregion
#region Load needed PowerShell modules
# Ensure PnP PowerShell is loaded
$minimumVersion = New-Object System.Version("2.24.1803.0")
if (-not (Get-InstalledModule -Name SharePointPnPPowerShellOnline -MinimumVersion $minimumVersion -ErrorAction Ignore))
{
Install-Module SharePointPnPPowerShellOnline -MinimumVersion $minimumVersion -Scope CurrentUser
}
Import-Module SharePointPnPPowerShellOnline -DisableNameChecking -MinimumVersion $minimumVersion
# Ensure Azure PowerShell is loaded
$loadAzurePreview = $false
if (-not (Get-Module -ListAvailable -Name AzureAD))
{
# Maybe the preview AzureAD PowerShell is installed?
if (-not (Get-Module -ListAvailable -Name AzureADPreview))
{
install-module azuread
}
else
{
$loadAzurePreview = $true
}
}
if ($loadAzurePreview)
{
Import-Module AzureADPreview
}
else
{
Import-Module AzureAD
}
#endregion
#region Gather Groupify run input
# Url of the site collection to remediate
$siteCollectionUrlToRemediate = ""
$siteAlias = ""
$siteIsPublic = $false
# Get the input information
$siteURLFile = Read-Host -Prompt 'Input either single site collection URL (e.g. https://contoso.sharepoint.com/sites/teamsite1) or name of .CSV file (e.g. sitecollections.csv) ?'
if (-not $siteURLFile.EndsWith(".csv"))
{
$siteCollectionUrlToRemediate = $siteURLFile
$siteAlias = Read-Host -Prompt 'Input the alias to be used to groupify this site ?'
$siteIsPublicString = Read-Host -Prompt 'Will the created Office 365 group be a public group ? Enter True for public, False otherwise'
$siteClassificationLabel = Read-Host -Prompt 'Classification label to use? Enter label or leave empty if not configured'
try
{
$siteIsPublic = [System.Convert]::ToBoolean($siteIsPublicString)
}
catch [FormatException]
{
$siteIsPublic = $false
}
}
# Get the tenant admin credentials.
$credentials = $null
$azureADCredentials = $null
$adminUPN = $null
if(![String]::IsNullOrEmpty($credentialManagerCredentialToUse) -and (Get-PnPStoredCredential -Name $credentialManagerCredentialToUse) -ne $null)
{
$adminUPN = (Get-PnPStoredCredential -Name $credentialManagerCredentialToUse).UserName
$credentials = $credentialManagerCredentialToUse
$azureADCredentials = Get-PnPStoredCredential -Name $credentialManagerCredentialToUse -Type PSCredential
}
else
{
# Prompts for credentials, if not found in the Windows Credential Manager.
$adminUPN = Read-Host -Prompt "Please enter admin UPN"
$pass = Read-host -AsSecureString "Please enter admin password"
$credentials = new-object management.automation.pscredential $adminUPN,$pass
$azureADCredentials = $credentials
}
if($credentials -eq $null)
{
Write-Host "Error: No credentials supplied." -ForegroundColor Red
exit 1
}
#endregion
#region Connect to SharePoint and Azure
# Get a tenant admin connection, will be reused in the remainder of the script
LogWrite "Connect to tenant admin site $tenantAdminUrl"
$tenantContext = Connect-PnPOnline -Url $tenantAdminUrl -Credentials $credentials -Verbose -ReturnConnection
LogWrite "Connect to Azure AD"
$azureUser = Connect-AzureAD -Credential $azureADCredentials
#endregion
#region Groupify the site(s)
if (-not $siteURLFile.EndsWith(".csv"))
{
# Remediate the given site collection
GroupifySite $siteCollectionUrlToRemediate $siteAlias $siteIsPublic $siteClassificationLabel $credentials $tenantContext $adminUPN
}
else
{
$csvRows = Import-Csv $siteURLFile
foreach($row in $csvRows)
{
if($row.Url.Trim() -ne "")
{
$siteUrl = $row.Url
$siteAlias = $row.Alias
$siteIsPublicString = $row.IsPublic
try
{
$siteIsPublic = [System.Convert]::ToBoolean($siteIsPublicString)
}
catch [FormatException]
{
$siteIsPublic = $false
}
$siteClassification = $row.Classification
if ($siteClassification -ne $null)
{
$siteClassification = $siteClassification.Trim(" ")
}
GroupifySite $siteUrl $siteAlias $siteIsPublic $siteClassification $credentials $tenantContext $adminUPN
}
}
}
#endregion
#region Close log files
if ($global:strmWrtLog -ne $NULL)
{
$global:strmWrtLog.Close()
$global:strmWrtLog.Dispose()
}
if ($global:strmWrtError -ne $NULL)
{
$global:strmWrtError.Close()
$global:strmWrtError.Dispose()
}
#endregion