Se connecter à un groupe Microsoft 365

La possibilité de connecter un groupe Microsoft 365 à un site SharePoint existant est primordiale pour moderniser ce site. Une fois que votre site est connecté à un groupe Microsoft 365, il peut bénéficier de tous les autres services connectés à un groupe tels que Microsoft Teams et Planificateur. Cette connexion donne à votre site classique une approche similaire au site d’équipe moderne actuel, qui est connecté à un groupe Microsoft 365 par défaut.

Vous pouvez connecter votre site à un nouveau groupe Microsoft 365 à partir de l’utilisateur d’interface, et ce site par site, ce qui est préférable pour les environnements de petite taille. Toutefois, les clients importants souhaitent généralement offrir une expérience homogène à leurs utilisateurs, et par conséquent effectuer une opération en bloc sur leurs sites.

Dans cet article, vous allez découvrir comment vous préparer pour une opération en bloc en associant des sites à des groupes Microsoft 365 et comment réellement réussir.

Important

  • Désolé... Nous ne prenons pas en charge la connexion d’un site de communication au groupe Microsoft 365.
  • Vous ne pouvez pas connecté dans un groupe la collection de sites racine de votre client.

Important

Les outils de modernisation et tous les autres composants PnP sont des outils open source bénéficiant d’une prise en charge par une communauté active. Il n’existe aucun contrat de niveau de service pour la prise en charge des outils open source des canaux de support Microsoft officiels.

Ce qu’apporte la connexion à un groupe Microsoft 365 à votre site

Lorsque vous connectez votre site à un nouveau groupe Microsoft 365, plusieurs choses se produisent :

  • Un groupe Microsoft 365 est créé et ce dernier est connecté à votre collection de sites.
  • Une page d’accueil moderne est créée sur votre site et utilisée comme page d’accueil du site.
  • Les propriétaires du groupe deviennent les administrateurs de la collection de sites.
  • Les propriétaires du groupe sont ajoutés au groupe Propriétaires de votre site.
  • Les membres du groupe sont ajoutés au groupe Membres de votre site.

Une fois connecté à un groupe Microsoft 365, le site se comporte comme un site d’équipe connecté à un groupe moderne. Lorsque vous autorisez des utilisateurs à accéder au groupe Microsoft 365 connecté, vous leur accordez également l’accès au site SharePoint : ils peuvent créer une instance Microsoft Teams dans la partie supérieure du site, intégrer le Planificateur, etc.

Connexion d’un groupe Microsoft 365 via l’interface utilisateur SharePoint

La première méthode visant à connecter un groupe Microsoft 365 à votre site consiste à utiliser l’option disponible dans l’interface utilisateur. En cliquant sur l’icône d’engrenage dans la barre de navigation, vous pouvez sélectionner l’option Se connecter à un nouveau groupe Microsoft 365, qui lance un assistant. Celui-ci vous guide tout au long du processus de « groupify », comme illustré dans les captures d’écran ci-dessous.

Menu des actions du site (icône engrenage)

Actions du site


Assistant

Assistant

Connexion d’un groupe Microsoft 365 par programme

Pour connecter par programme un groupe Microsoft 365, nous vous recommandons de suivre un processus en trois étapes :

  • Apprendre
  • Analyse
  • Moderniser

Étape 1 : Découvrez ce qu’une connexion de groupe apporte à votre site

Il est important de vous familiariser avec le processus « groupify » et par conséquent, nous vous recommandons d’exécuter manuellement « groupify » pour certains sites de test en utilisant l’option de l’interface utilisateur. L’un des aspects essentiels à évaluer est de conserver la page d’accueil moderne récemment créée ou pas. Dans le cadre du script de modernisation, vous pouvez créer une page d’accueil personnalisée. Cependant, si la page créée par défaut répond à vos besoins, conservez-la comme option préférée.

Étape 2 : Analyser vos sites

L’option d’interface utilisateur illustrée dans la section précédente n’est pas appropriée si vous voulez grouper des centaines de collections de sites. À ce stade, l’utilisation d’une API pour effectuer l’opération par programme est plus adéquate. Avant de continuer, veillez à vérifier quels sites sont prêts à être groupés (« groupify »), car tous les sites ne sont pas adaptés à ce processus.

Pour vous aider à déterminer les sites qui sont prêts à être groupés (« groupify »), vous pouvez utiliser le scanneur de modernisation de SharePoint afin d’analyser votre environnement. Ce lien contient tous les détails nécessaires pour exécuter le scanneur. Une fois que vous avez exécuté le scanneur, accédez à l’article permettant de comprendre et traiter les résultats du scanneur pour analyser les résultats d’analyse.

Étape 3 : Moderniser vos sites

Le processus de « groupify » en bloc se compose de deux étapes :

  • Préparer et valider un fichier d’entrée que vous utiliserez pour réaliser le processus « groupify » en bloc.
  • Exécuter le processus « groupify » en bloc.

Création d’un fichier d’entrée pour le processus « groupify » en bloc et validation de ce fichier

Après avoir exécuté le scanneur et traité les résultats, vous avez identifié les sites prêts à se connecter au groupe. L’étape suivante consiste à préparer un fichier CSV pour piloter le processus de connexion de groupe en bloc. Le format de fichier CSV est simple :

  • Colonne URL : contient l’URL de la collection de sites à laquelle appliquer le processus « groupify ».
  • Alias contient l’alias du groupe Microsoft 365 à utiliser. Cet alias ne peut pas contenir d’espaces et ne doit pas avoir déjà été utilisé.
  • IsPublic indique si le site doit être public ou privé.
  • Classification contient la classification de site que vous voulez définir après l’exécution du processus « groupify » sur le site. Cette opération est requise car, après la connexion à un groupe, cette classification est conservée au niveau du groupe Microsoft 365.

Voici un court exemple :

Url,Alias,IsPublic,Classification
https://contoso.sharepoint.com/sites/hrteam,hrteam,false,Medium Impact
https://contoso.sharepoint.com/sites/engineering,engineeringteam,true,Low Impact

Pour vous aider à vérifier ce fichier avant de l’utiliser, vous pouvez utiliser le script PowerShell ci-dessous. Ce script vérifie l’existence d’alias et d’URL de site valides. Mettez à jour ce script avec l’URL de votre centre d’administration client et exécutez-le. Le script vous invite à indiquer le nom du fichier CSV et génère un rapport.

Pendant l’exécution du script de validation, les erreurs suivantes peuvent apparaître :

Erreur Description
Stratégie d’attribution de noms AzureAD : PrefixSuffix contient des attributs AD résolus en fonction de l’utilisateur qui exécute la connexion de groupe. Dans Azure AD, vous pouvez définir une stratégie d’attribution de noms pour les groupes Microsoft 365. Si ces noms contiennent des attributs Active Directory d’utilisateur, cela peut être un problème, car le processus « groupify » en bloc gère tous les sites à l’aide de l’utilisateur actuel.
Stratégie de création AzureAD : adminUPN ne fait pas partie du groupe CanCreateGroupsId qui détermine la création de groupes Microsoft 365. Si la création de groupes Azure AD est restreinte pour certains comptes et le compte actuel n’en fait pas partie, la création de groupe Microsoft 365 échoue.
siteUrl : l’alias [siteAlias] contient un espace, ce qui n’est pas autorisé. L’alias d’un groupe Microsoft 365 ne peut pas contenir d’espace.
siteUrl : la classification [siteClassification] ne correspond pas aux classifications Azure AD disponibles [ClassificationListString]. La classification du site fournie n’est pas définie comme faisant partie des classifications de site autorisées pour les groupes Microsoft 365.
siteUrl : l’alias [siteAlias] figure dans la liste des mots Azure AD bloqués [CustomBlockedWordsListString]. Si une liste de mots bloqués est configurée dans Azure AD et le nom du groupe Microsoft 365 fourni utilise ce mot, cette erreur est générée.
siteUrl : le site est déjà connecté à un groupe. Un site peut uniquement être connecté à un seul groupe Microsoft 365. Donc une fois connecté, il ne peut plus être connecté à un autre groupe.
siteUrl : l’alias [siteAlias] est déjà en cours d’utilisation. Chaque groupe Microsoft 365 doit disposer d’un alias unique. Une erreur est générée lorsque l’alias proposé est déjà utilisé par un autre groupe Microsoft 365.
siteUrl : l’alias [siteAlias] était déjà marqué comme alias approuvé pour un autre site dans ce fichier. L’alias du site proposé a déjà été défini pour un autre site dans les versions antérieures des fichiers CSV « groupify » en bloc.
siteUrl : le site n’existe pas ou n’est pas disponible (état = site. État) L’URL du site fournie ne représente pas une collection de sites accessibles.

Notes

Veuillez mettre à jour la variable $tenantAdminUrl dans le script pour qu’il contienne l’URL de votre centre d’administration client (par exemple, https://contoso-admin.sharepoint.com).

Au cours de l’exécution du script, un fichier journal est généré et associé à un fichier d’erreur qui contient un sous-ensemble du fichier journal (contenant uniquement les erreurs).

<#
.SYNOPSIS
Validates the CSV input file for the bulk "Office 365 Group Connects". 

.EXAMPLE
PS C:\> .\ValidateInput.ps1
#>

#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 + "\GroupconnectInputValidation_log_" + $date.ToFileTime() + ".txt")
$global:strmWrtLog=[System.IO.StreamWriter]$logfile
$global:Errorfile = ((Get-Item -Path ".\" -Verbose).FullName + "\GroupconnectInputValidation_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 group connect
        # 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 group connect"
    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

Lancer le processus « groupify » en bloc.

Maintenant que nous avons un fichier d’entrée qui définit les sites auxquels appliquer « groupify », nous pouvons enfin exécuter ce processus. Le script PowerShell suivant est un exemple de script que vous pouvez ajuster à vos besoins, si vous souhaitez plus ou moins d’éléments dans le cadre de la connexion de groupe.

La version partagée de l’exemple de script met en œuvre les étapes suivantes :

  • Ajouter un administrateur client actuel en tant qu’administrateur de site en cas de besoin : « groupify » nécessite un compte d’utilisateur (et pas seulement une application).
  • Vérifier l’utilisation du modèle de site / fonctionnalité de publication, et empêcher groupify, conformément à la logique du scanneur.
  • Vérifier qu’aucune fonctionnalité de blocage de l’interface utilisateur moderne n’est activée. Sinon, corriger le problème.
  • Vérifier que la fonctionnalité de page moderne est activée.
  • Facultatif : déployer des applications (par exemple, le personnalisateur d’application).
  • Facultatif : ajouter votre propre page d’accueil moderne.
  • Se connecter en groupe API.
  • Définir les administrateurs et les propriétaires de site comme propriétaires du groupe.
  • Définir les membres du site en tant que membres du groupe.
  • Supprimer les administrateurs client et les propriétaires de site ajoutés des administrateurs SharePoint.
  • Supprimer les administrateurs client ajoutés du groupe Microsoft 365.

Pour exécuter le script PowerShell ci-dessous, vous devez mettre à jour l’URL du centre d’administration client. Indiquez le fichier d’entrée CSV et les informations d’identification au moment de l’exécution.

Notes

Voici un exemple de script que vous devez ajuster à vos besoins en mettant à jour / supprimer les parties facultatives ou en ajoutant des tâches de modernisation supplémentaires (telles que la définition d'un thème de site SharePoint). Mettez à jour la variable $tenantAdminUrl dans le script pour qu’il contienne l’URL de votre centre d’administration client (par exemple, https://contoso-admin.sharepoint.com).

Au cours de l’exécution du script, un fichier journal est généré et associé à un fichier d’erreur qui contient un sous-ensemble du fichier journal (contenant uniquement les erreurs).

<#
.SYNOPSIS
"Office 365 Group Connects" a Classic SharePoint Online team site by attaching it to an Office Group and provisioning the default resources. Also enables the user to add a classification label and alias for the Group and enables Modern User Experience for the site.

Doesn't use parameters, rather asks for the values it needs. Optionally, supports hardcoding the use of Credential Manager (won't ask for credentials) and SharePoint admin site url.

.EXAMPLE
PS C:\> .\O365GroupConnectSite.ps1
#>

#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 GroupConnectSite
{
    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 group connect: $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
    {
        # Group connect steps
        # - [Done] Add current tenant admin as site admin when needed
        # - [Done] Verify site template / publishing feature use and prevent group connect --> 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 group connect 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 group connect this site"
        }

        # Grab the web template and verify if it's a group connect 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 group connect 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 group connect API
        LogWrite "Call group connnect API with following settings: Alias=$alias, IsPublic=$isPublic, Classification=$siteClassification"
        Add-PnPOffice365GroupToSite -Url $siteCollectionUrl -Alias $alias -DisplayName $alias -Description $web.Description -IsPublic:$isPublic -KeepOldHomePage:$false -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 "Group connect 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 "Group connect 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
        # Group connect 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 Group connect 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 "Group connect failed for site collection $siteCollectionUrl" Red
    } 

}

#######################################################
# MAIN section                                        #
#######################################################

# OVERRIDES
# If you want to automate the run and make the script ask less questions, feel free to hardcode these 2 values below. Otherwise they'll be asked from the user or parsed from the values they input

# Tenant admin url
$tenantAdminUrl = "" # e.g. "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 + "\Groupconnect_log_" + $date.ToFileTime() + ".txt")
$global:strmWrtLog=[System.IO.StreamWriter]$logfile
$global:Errorfile = ((Get-Item -Path ".\" -Verbose).FullName + "\Groupconnect_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("1.3.0")
if (-not (Get-InstalledModule -Name PnP.PowerShell -MinimumVersion $minimumVersion -ErrorAction Ignore)) 
{
    Install-Module PnP.PowerShell -MinimumVersion $minimumVersion -Scope CurrentUser
}
Import-Module PnP.PowerShell -DisableNameChecking -MinimumVersion $minimumVersion

# Ensure Azure PowerShell is loaded
$loadAzurePreview = $false # false to use 2.x stable, true to use the preview versions of cmdlets
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 group connect 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 group connect 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
    }
}
# If we are using a CSV, we'll need to get the tenant admin url from the user or use the hardcoded one
else {
    if ($tenantAdminUrl -eq $null -or $tenantAdminUrl.Length -le 0) {
        $tenantAdminUrl = Read-Host -Prompt 'Input the tenant admin site URL (like https://contoso-admin.sharepoint.com): '
    }
}

# We'll parse the tenantAdminUrl from site url (unless it's set already!)
if ($tenantAdminUrl -eq $null -or $tenantAdminUrl.Length -le 0) {
    if ($siteURLFile.IndexOf("/teams") -gt 0) {
        $tenantAdminUrl = $siteURLFile.Substring(0, $siteURLFile.IndexOf("/teams")).Replace(".sharepoint.", "-admin.sharepoint.")
    }
    else {
        $tenantAdminUrl = $siteURLFile.Substring(0, $siteURLFile.IndexOf("/sites")).Replace(".sharepoint.", "-admin.sharepoint.")
    }
}

# 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 Group connect the site(s)
if (-not $siteURLFile.EndsWith(".csv"))
{
    # Remediate the given site collection
    GroupConnectSite $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(" ")
            }

            GroupConnectSite $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

Voir aussi