Public/Test-SPMTenantCapability.ps1
|
function Test-SPMTenantCapability { <# .SYNOPSIS Mesure ce que le tenant AUTORISE, au lieu de le deviner. .DESCRIPTION Le pré-vol énumère, vérifie les noms, les tailles et les longueurs d'URL, mais n'écrit rien : il ne dit jamais si la copie serveur fonctionne sur ce tenant, ni si le compte peut y créer une bibliothèque. Toute une session a été perdue à confondre un bug d'appel client avec un refus du service — faute de cette mesure. Deux étages, séparés parce qu'ils n'engagent pas la même chose : * LECTURE SEULE, toujours. Connexions, droits effectifs à destination (créer une bibliothèque, ajouter des éléments, appliquer un modèle de site), présence de la bibliothèque cible. * SONDE DE COPIE, seulement sur -ProbeCopy. Crée un dossier temporaire à destination, y copie le plus petit fichier de la source, supprime le dossier. C'est le SEUL moyen de savoir si l'API de travaux de copie est autorisée — la question qui décide de tout le reste, et qui coûte trente secondes. Les trois opérations de la sonde sont jugées séparément. Les confondre faisait lire « Accès refusé » comme un bridage de l'API de copie alors que le refus portait sur la création du dossier : un diagnostic radicalement différent, et une conclusion fausse sur la préservation des versions. .PARAMETER SourceUrl URL de la bibliothèque ou du dossier source. .PARAMETER TargetUrl URL du dossier de destination. .PARAMETER ProbeCopy Exécute la sonde de copie. SEUL mode qui écrit — dans un dossier temporaire supprimé ensuite. .EXAMPLE Test-SPMTenantCapability -SourceUrl $src -TargetUrl $dst -ProbeCopy .OUTPUTS Objet { Ok, ServerCopy, CanCreateList, CanApplySiteTemplate, RecommendedTransfer, RecommendedSchemaMethod, Checks }. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, Position = 0)][string]$SourceUrl, [Parameter(Mandatory, Position = 1)][string]$TargetUrl, $SourceConnection, $DestinationConnection, [switch]$ProbeCopy, [string]$ClientId = $env:SPM_CLIENT_ID, [string]$Tenant = $env:SPM_TENANT, [string]$Thumbprint = $env:SPM_CERT_THUMBPRINT ) $checks = [System.Collections.Generic.List[object]]::new() $ajout = { param([string]$Nom, [string]$Etat, [string]$Detail) $checks.Add([pscustomobject]@{ Check = $Nom; State = $Etat; Detail = $Detail }) $niveau = switch ($Etat) { 'OK' { 'SUCCESS' } 'ECHEC' { 'ERROR' } 'IGNORE' { 'DEBUG' } default { 'WARNING' } } Write-SPMLog -Level $niveau -Operation 'Capability' -Message "$Nom : $Etat — $Detail" } # ------------------------------------------------------------- connexions if (-not $DestinationConnection -or (-not $SourceConnection)) { $splat = @{ ClientId = $ClientId } if ($Thumbprint) { if (-not $Tenant) { throw 'Tenant est obligatoire avec un certificat.' } $splat.Thumbprint = $Thumbprint; $splat.Tenant = $Tenant } else { $splat.Interactive = $true } if (-not $SourceConnection) { $SourceConnection = Connect-SPMSite @splat -Url (Get-SPMSiteUrlFromResourceUrl -Url $SourceUrl) } if (-not $DestinationConnection) { $DestinationConnection = Connect-SPMSite @splat -Url (Get-SPMSiteUrlFromResourceUrl -Url $TargetUrl) } } & $ajout 'Connexions' 'OK' 'source et destination ouvertes' $srcPath = Get-SPMServerRelativePath -Url $SourceUrl $dstPath = Get-SPMServerRelativePath -Url $TargetUrl # ------------------------------------------------------- droits effectifs # Mesurer les droits AVANT d'écrire coûte un appel et évite de conclure # « API refusée » sur un compte qui n'a simplement pas le droit de créer. # Un droit non déterminé reste $null : « inconnu » n'est pas « refusé ». $peutCreerListe = $null $peutModeleDeSite = $null try { $web = Invoke-SPMWithRetry -Operation 'web/capability' -ScriptBlock { Get-PnPWeb -Includes EffectiveBasePermissions -Connection $DestinationConnection -ErrorAction Stop } $perms = $web.PSObject.Properties['EffectiveBasePermissions'] ? $web.EffectiveBasePermissions : $null if ($perms -and $perms.PSObject.Methods['Has']) { $peutCreerListe = [bool]$perms.Has('ManageLists') $peutModeleDeSite = [bool]$perms.Has('ManageWeb') $etatListe = if ($peutCreerListe) { 'OK' } else { 'ECHEC' } $detailListe = if ($peutCreerListe) { 'droit ManageLists accorde' } else { 'droit ManageLists absent : Copy-SPSiteSchema echouerait' } & $ajout 'Creation de bibliotheque' $etatListe $detailListe $etatModele = if ($peutModeleDeSite) { 'OK' } else { 'AVERTISSEMENT' } $detailModele = if ($peutModeleDeSite) { 'droit ManageWeb accorde' } else { 'droit ManageWeb absent : Invoke-PnPSiteTemplate echouerait' } & $ajout 'Modele de site' $etatModele $detailModele } else { & $ajout 'Droits effectifs' 'IGNORE' 'permissions non lisibles sur ce tenant : ni accordees ni refusees' } } catch { & $ajout 'Droits effectifs' 'IGNORE' ($_.Exception.Message -split [char]10)[0] } # ------------------------------------------- bibliotheque de destination $libTitle = $null try { $libTitle = Get-SPMListTitleForPath -ServerRelativeUrl $dstPath -Connection $DestinationConnection } catch { $libTitle = $null } $destinationPrete = [bool]$libTitle $etatDest = if ($destinationPrete) { 'OK' } else { 'ECHEC' } $detailDest = if ($destinationPrete) { "« $libTitle »" } else { "aucune liste ne porte $dstPath" } & $ajout 'Bibliotheque de destination' $etatDest $detailDest # ------------------------------------------------------- sonde de copie $copieServeur = 'non testé' if (-not $ProbeCopy) { & $ajout 'Copie serveur-a-serveur' 'IGNORE' 'relancez avec -ProbeCopy pour trancher' } elseif (-not $destinationPrete) { # Créer un dossier sous un chemin qui n'appartient à aucune liste donne # « Accès refusé », qu'on lirait alors comme un bridage de l'API de # copie. Ce faux signal a coûté assez cher pour qu'on l'empêche. & $ajout 'Copie serveur-a-serveur' 'IGNORE' 'destination absente : la sonde ne prouverait rien' } else { Clear-SPMEnumerationFailure $items = @(Get-SPMFolderInventory -FolderUrl $srcPath -Connection $SourceConnection -MaxDepth 1) $candidats = @($items | Where-Object { -not $_.IsFolder -and $_.SizeBytes -gt 0 }) if (-not $candidats.Count) { $candidats = @($items | Where-Object { -not $_.IsFolder }) } $cobaye = $candidats | Sort-Object SizeBytes | Select-Object -First 1 if (-not $cobaye) { & $ajout 'Copie serveur-a-serveur' 'IGNORE' 'aucun fichier a la source' } elseif (-not $PSCmdlet.ShouldProcess($TargetUrl, 'Sonder la copie serveur (dossier temporaire)')) { & $ajout 'Copie serveur-a-serveur' 'IGNORE' 'sonde simulee' } else { $sondeNom = "_sonde_$(Get-Date -Format 'yyyyMMdd_HHmmss')" $sondeServeur = "$dstPath/$sondeNom" $sondeSiteRel = $null $dossierCree = $false try { $sondeSiteRel = Get-SPMSiteRelativePath -ServerRelativeUrl $sondeServeur -Connection $DestinationConnection Invoke-SPMWithRetry -Operation 'Resolve-PnPFolder/sonde' -MaxRetries 0 -ScriptBlock { Resolve-PnPFolder -SiteRelativePath $sondeSiteRel -Connection $DestinationConnection -ErrorAction Stop } | Out-Null $dossierCree = $true & $ajout 'Creation du dossier sonde' 'OK' $sondeSiteRel } catch { & $ajout 'Creation du dossier sonde' 'ECHEC' "$(($_.Exception.Message -split [char]10)[0]) (chemin : $sondeSiteRel)" } if ($dossierCree) { try { Invoke-SPMWithRetry -Operation 'Copy-PnPFile/sonde' -MaxRetries 0 -ScriptBlock { Copy-PnPFile -SourceUrl $cobaye.ServerRelativeUrl -TargetUrl $sondeServeur ` -Force -Connection $SourceConnection -ErrorAction Stop } | Out-Null $copieServeur = 'oui' & $ajout 'Copie serveur-a-serveur' 'OK' "autorisee sur « $($cobaye.Name) » — versions recuperables" } catch { $msg = ($_.Exception.Message -split [char]10)[0] if ($msg -match 'Unauthorized|Access denied|Acces refuse|Accès refusé|403') { $copieServeur = 'non' & $ajout 'Copie serveur-a-serveur' 'ECHEC' 'REFUSEE par le tenant — versions non recuperables sans repli' } else { # Un échec qui n'est pas un refus ne prouve rien sur # l'API : le dire « refusée » condamnerait à tort la # préservation des versions. & $ajout 'Copie serveur-a-serveur' 'AVERTISSEMENT' "echec non concluant : $msg" } } try { $parent = '' if ($sondeSiteRel.Contains('/')) { $parent = $sondeSiteRel.Substring(0, $sondeSiteRel.LastIndexOf('/')) } Invoke-SPMWithRetry -Operation 'Remove-PnPFolder/sonde' -MaxRetries 0 -ScriptBlock { Remove-PnPFolder -Name $sondeNom -Folder $parent -Force ` -Connection $DestinationConnection -ErrorAction Stop } | Out-Null & $ajout 'Suppression du dossier sonde' 'OK' 'destination laissee intacte' } catch { & $ajout 'Suppression du dossier sonde' 'AVERTISSEMENT' ` "a supprimer a la main : $sondeServeur" } } } } $echecs = @($checks | Where-Object State -eq 'ECHEC') [pscustomobject]@{ Ok = ($echecs.Count -eq 0) ServerCopy = $copieServeur # La soumission part de la SOURCE : CreateCopyJobs s'invoque sur le site # qui détient le contenu à exporter. Demander à la cible d'exporter un # contenu qu'elle n'a pas était le défaut d'origine du moteur. SubmitFrom = 'source' CanCreateList = $peutCreerListe CanApplySiteTemplate = $peutModeleDeSite DestinationLibrary = $libTitle RecommendedTransfer = switch ($copieServeur) { 'oui' { 'copie serveur — historique préservé' } 'non' { 'repli local obligatoire — HISTORIQUE PERDU' } default { 'inconnu — sonde non exécutée' } } RecommendedSchemaMethod = if ($peutModeleDeSite) { 'Copy-SPSiteStructure (modèle de site)' } elseif ($peutCreerListe) { 'Copy-SPSiteSchema (création directe)' } else { 'inconnu — droits non mesurés' } Checks = $checks.ToArray() Failures = $echecs.Count } } |