Compartilhar via


Conectar-se a um grupo Microsoft 365

Ser capaz de conectar um grupo do Microsoft 365 a um site existente do Microsoft Office SharePoint Online é importante se você quiser modernizar esse site. Depois que o seu site for conectado a um grupo do Microsoft 365, ele poderá se beneficiar de todos os outros serviços conectados ao grupo, como Microsoft Teams e Planner. Essa conexão também traz o seu site clássico um passo mais perto de ser como o site de equipe moderno atual, que por padrão está conectado a um grupo do Microsoft 365.

Você pode conectar o seu site a um novo grupo do Microsoft 365 a partir da interface do usuário site a site, o que pode ser bom para ambientes menores. No entanto, clientes maiores normalmente querem oferecer uma experiência consistente para seus usuários e, portanto, querem realizar uma operação em massa de seus sites.

Neste artigo, você aprenderá como se preparar para uma operação em massa para associar sites a novos grupos do Microsoft 365 e como realmente fazer isso acontecer.

Importante

  • não tem suporte para conectar um site de comunicação ao grupo Microsoft 365.
  • Não é possível conectar o conjunto de sites raiz ao seu locatário.

Importante

As ferramentas de modernização e todos os outros componentes PnP são ferramentas de código aberto, sustentadas por uma comunidade ativa que fornece suporte a eles. Não há SLA para suporte de ferramentas de código aberto a partir dos canais oficiais de suporte da Microsoft.

O que a conexão com um novo grupo Microsoft 365 faz ao seu site

Quando você conecta o seu site a um novo grupo do Microsoft 365, uma série de coisas acontecem:

  • Um novo grupo do Microsoft 365 é criado e esse grupo é conectado ao seu conjunto de sites
  • Uma nova home page moderna será criada em seu site e definida como sua home page
  • Os proprietários do grupo serão os administradores do conjunto de sites
  • Os proprietários do grupo serão adicionados ao grupo de proprietários do seu site
  • Os membros do grupo serão adicionados ao grupo de membros do seu site

Depois que o seu site é conectado a um grupo do Microsoft 365, ele se comporta como um site de equipe conectado a um grupo moderno, portanto, conceder permissão às pessoas para o grupo do Microsoft 365 conectado agora também lhes concede acesso ao site do Microsoft Office SharePoint Online, uma equipe da Microsoft pode ser criada no topo do site, o Planner pode ser integrado e assim por diante.

Conectar um grupo Microsoft 365 usando a interface de usuário do Microsoft Office SharePoint Online

Uma abordagem para conectar um grupo do Microsoft 365 ao seu site é usar a opção disponível na interface do usuário. Selecionando o ícone de engrenagem na barra de navegação, você pode selecionar a opção Conectar ao novo grupo Microsoft 365, que inicia um assistente que o orienta no processo de conexão de grupo, conforme mostrado em as seguintes imagens.

Menu de ações do site (ícone de engrenagem)

Ações do site


Assistente

Assistente

Conectar programaticamente um grupo Microsoft 365

Para conectar programaticamente um grupo do Microsoft 365, recomendamos que você siga um processo de três etapas:

  • Saiba mais
  • Analisar
  • Modernizar

Etapa 1: saiba o que uma conexão ao grupo faz ao seu site

É importante se familiarizar com o que uma conexão ao grupo faz ao seu site, assim, é recomendável a realização de uma conexão ao grupo manual para alguns sites de teste usando a opção de interface do usuário. Um aspecto importante a ser avaliado é se você deseja manter a home page moderna recém-criada. Como parte do script de modernização, será possível criar uma home page sob medida, mas se a padrão atender às suas necessidades, essa será a opção preferida.

Etapa 2: analise seus sites

A opção de interface do usuário mostrada na seção anterior não será adequada se você quiser conectar centenas de conjuntos de sites ao grupo. Nesse ponto, faz muito sentido usar uma API para fazer isso programaticamente. Porém, antes de fazer isso, é melhor verificar quais sites estão prontos para a serem conectados ao grupo, já que nem todos os sites são adequados para isso.

Para ajudar você a entender quais sites estão prontos para serem conectados ao grupo, use o Verificador de modernização do SharePoint para analisar seu ambiente. Esse link contém todos os detalhes necessários para executar o verificador. Depois de executar o verificador, acesse o artigo Entender e processar os resultados do verificador para analisar os resultados da verificação.

Etapa 3: modernize seus sites

O processo de conexão a grupo em massa é composto por duas etapas:

  • Prepare e valide um arquivo de entrada que você usará para conduzir o processo de conexão a grupo.
  • Execute o processo de conexão a grupo em massa.

Crie um arquivo de entrada para a conexão a grupo em massa e valide-o

Depois de executar o verificador e processar os resultados, você identificou quais sites estão prontos para conexão a um grupo. A próxima etapa será preparar um arquivo CSV para conduzir o processo de conexão a grupo em massa. O formato de arquivo CSV é simples:

  • A coluna URL contém a URL para o conjunto de sites a ser conectado a um grupo.
  • Alias contém o alias do grupo Microsoft 365 que você deseja usar. Esse alias não pode conter espaços e já deve ter sido usado anteriormente.
  • IsPublic indica se você quer que o site seja público ou privado.
  • Classificação contém a classificação de site que você deseja usar após a conexão a um grupo. Isso é necessário porque, depois de conectado a um grupo, essa classificação é mantida no nível de grupo do Microsoft 365.

A seguir, há um pequeno exemplo:

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

Para ajudar a verificar esse arquivo antes de usá-lo, é possível usar o script do PowerShell que está no final desta seção. Esse script verifica se há URLs e aliases válidos para o site. Atualize esse script com a URL do centro de administração de locatário e execute-o. O script solicita o nome do arquivo CSV e gera um relatório para você.

Durante a execução do script de validação, estes erros podem surgir:

Erro Descrição
Política de nomenclatura do AzureAD: o PrefixSuffix contém atributos do AD que são resolvidos com base no usuário que está executando a conexão de grupo No Azure Active Directory, você pode definir uma política de nomenclatura para grupos do Microsoft 365. Se esta política contiver atributos de usuário do Active Directory, isso pode ser um problema porque a conexão de grupo em massa lida com todos os sites usando o usuário atual.
Política de criação do AzureAD: adminUPN não faz parte do grupo CanCreateGroupsId que controla a criação do grupo Microsoft 365 Se a criação do grupo do Azure Active Directory for restrita a certas contas e a conta atual não estiver entre elas, a criação do grupo do Microsoft 365 falhará.
siteUrl : Alias [siteAlias] contém um espaço, o que não é permitido O alias de um grupo Microsoft 365 não pode conter um espaço.
siteUrl : Classificação [siteClassification] não está em conformidade com as classificações do Azure Active Directory disponíveis [ClassificationListString] A classificação de site fornecida não é definida como uma das classificações de site permitidas para grupos Microsoft 365.
siteUrl : Alias [siteAlias] está na lista de palavras bloqueadas do Azure Active Directory [CustomBlockedWordsListString] Se uma lista de palavras bloqueadas for configurada no Azure Active Directory e o nome de grupo do Microsoft 365 fornecido usar essa palavra, esse erro será gerado.
siteUrl : o site já está conectado a um grupo Um site só pode ser conectado a um único grupo do Microsoft 365, portanto, depois que um site é conectado, ele não pode mais ser conectado a um grupo.
siteUrl : Alias [siteAlias] já está em uso Cada grupo do Microsoft 365 precisa de um alias exclusivo; um erro é gerado quando o alias proposto já foi usado por outro grupo Microsoft 365.
siteUrl: o alias [siteAlias] já foi marcado como alias aprovado para outro site neste arquivo O alias de site proposto já foi definido para outro site em linhas de entrada anteriores do arquivo CSV da conexão a grupo em massa.
siteUrl: o site não existe ou não está disponível (status = site.Status) A URL de site fornecida não representa um conjunto de sites acessível.

Observação

Atualize a variável $tenantAdminUrl no script a seguir para incluir a URL do centro de administração de locatários (por exemplo, https://contoso-admin.sharepoint.com).

Durante a execução do script, um arquivo de log é gerado junto com um arquivo de erro que contém um subconjunto do arquivo de log (somente os erros).

<#
.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

Executar o processo de conexão a grupo em massa

Agora que temos um arquivo de entrada que define os sites que precisam conectados a um grupo, podemos finalmente executá-lo. O seguinte script do PowerShell é um script de exemplo que pode ser adaptado às suas necessidades caso você deseje que mais ou menos coisas façam parte da conexão ao grupo.

A versão de exemplo compartilhada do script implementa as seguintes etapas:

  • Adição do administrador de locatários atual como administrador do site quando for necessário; a conexão a um grupo requer uma conta de usuário (portanto, não é somente aplicativo).
  • Verificação do uso do modelo de site/recurso de publicação e impedimento da conexão a um grupo; alinha-se à lógica do verificador.
  • Verificação de se algum recurso de bloqueio moderno está habilitado e, caso algum esteja, é corrigido.
  • Verificação de se o recurso de página moderna está habilitado.
  • Opcional: implantação de aplicativos (por exemplo, personalizador de aplicativo).
  • Opcional: adição de uma home page moderna.
  • Chamada à API de conexão ao grupo.
  • Definição dos administradores e proprietários do site como proprietários de grupo.
  • Definição dos membros do site como membros do grupo.
  • Remoção de administradores de locatário e proprietários de sites adicionados nos administradores do SharePoint.
  • Remove o administrador do locatário adicionado do grupo Microsoft 365.

A execução do script do PowerShell a seguir requer que você atualize a URL do centro de administração de locatários e, no tempo de execução, forneça as credenciais e o arquivo CSV de entrada.

Observação

Este é um script de exemplo que você deve adaptar de acordo com as suas necessidades. Para isso, atualize ou remova as partes opcionais ou adicione outras tarefas de modernização (como a configuração de um tema de site do SharePoint). Atualize a variável $tenantAdminUrl no script para incluir a URL do centro de administração de locatários (por exemplo, https://contoso-admin.sharepoint.com).

Durante a execução do script, um arquivo de log é gerado junto com um arquivo de erro que contém um subconjunto do arquivo de log (somente os erros).

<#
.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

Confira também