Src/Private/Connect-SharePointSession.ps1
|
function Connect-SharePointSession { <# .SYNOPSIS Establishes authenticated connections to Microsoft Graph and SharePoint Online. .NOTES Version: 0.1.7 Author: Pai Wei Sing #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string]$UserPrincipalName, [Parameter(Mandatory)] [string]$TenantAdminUrl ) if (-not (Test-UserPrincipalName -UserPrincipalName $UserPrincipalName)) { throw "Invalid User Principal Name format: '$UserPrincipalName'. Expected format: user@domain.com" } Write-TranscriptLog "Starting connection for SharePoint report: $UserPrincipalName" 'INFO' 'AUTH' $script:CachedOrg = $null $script:CachedSkus = $null $script:CachedLabels = @() #region ── Microsoft Graph ────────────────────────────────────────────────── $ExistingGraph = $null try { $ExistingGraph = Get-MgContext -ErrorAction SilentlyContinue } catch { } if ($ExistingGraph -and $ExistingGraph.TenantId) { Write-TranscriptLog "Reusing existing Microsoft Graph session (TenantId: $($ExistingGraph.TenantId))" 'SUCCESS' 'AUTH' } else { Write-Host " - Connecting to Microsoft Graph..." Write-TranscriptLog "Connecting to Microsoft Graph" 'INFO' 'AUTH' # Only the minimum scope required for core functionality. # AuditLog.Read.All and InformationProtectionPolicy.Read.All are optional -- # they require additional consent and cause auth failures on some tenants. # Those sections gracefully degrade when data is unavailable. $GraphScopes = @( 'Organization.Read.All' ) # Resolve the tenant GUID from the tenant domain using the public # OpenID configuration endpoint (no authentication required). # Passing -TenantId to Connect-MgGraph lets MSAL look up a cached # token for this specific tenant first, enabling silent re-auth # without a WAM popup when a valid token already exists in the cache. $TenantGuid = $null try { $TenantPrefix = ($TenantAdminUrl -replace 'https://', '' -replace '-admin\.sharepoint\.com.*', '') $OidcUrl = "https://login.microsoftonline.com/$TenantPrefix.onmicrosoft.com/v2.0/.well-known/openid-configuration" $OidcResp = Invoke-RestMethod -Uri $OidcUrl -Method GET -ErrorAction Stop # issuer looks like https://login.microsoftonline.com/{guid}/v2.0 $TenantGuid = ($OidcResp.issuer -split '/') | Where-Object { $_ -match '^[0-9a-f-]{36}$' } | Select-Object -First 1 if ($TenantGuid) { Write-Host " - Tenant GUID resolved: $TenantGuid" -ForegroundColor DarkGray } } catch { Write-TranscriptLog "Could not resolve tenant GUID (non-fatal): $($_.Exception.Message)" 'WARNING' 'AUTH' } $ConnectParams = @{ Scopes = $GraphScopes; ErrorAction = 'Stop' } if ($TenantGuid) { $ConnectParams['TenantId'] = $TenantGuid } # Try interactive browser first. # If WAM fails (known issue with Graph SDK v2.x on some Windows builds), # fall back to device code. Device code output goes directly to the console # host via [Console]::Write which PScribo does NOT capture -- unlike # Write-Host/Write-Information which PScribo intercepts. $GraphConnected = $false try { Connect-MgGraph @ConnectParams -ErrorAction Stop $GraphConnected = $true } catch { if ($_.Exception.Message -like '*InteractiveBrowserCredential*' -or $_.Exception.Message -like '*authentication failed*') { Write-Host " - Interactive browser auth failed. Falling back to device code..." -ForegroundColor Yellow Write-Host " - Open https://microsoft.com/devicelogin and enter the code below." -ForegroundColor Yellow $ConnectParams['UseDeviceCode'] = $true Connect-MgGraph @ConnectParams -ErrorAction Stop $GraphConnected = $true } else { throw } } $MgCtx = Get-MgContext -ErrorAction SilentlyContinue if ($MgCtx -and $MgCtx.TenantId) { Write-Host " - Microsoft Graph connected successfully." -ForegroundColor Green Write-TranscriptLog "Microsoft Graph connection verified (TenantId: $($MgCtx.TenantId))" 'SUCCESS' 'AUTH' } else { throw "Connect-MgGraph succeeded but Get-MgContext returned no context." } } #endregion #region ── Pre-fetch Graph data BEFORE PnP connects ───────────────────────── # PnP.PowerShell v3 loads Microsoft.Graph.Core v1.25.1 which conflicts with # Graph SDK v2.x (needs Core v3.x). After PnP connects ALL Graph SDK calls fail. # Fetch everything needed now while Graph SDK still works. Write-Host " - Pre-fetching Graph data before PnP connects..." -ForegroundColor Cyan try { $OrgResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/organization?$select=id,displayName,verifiedDomains,countryLetterCode,preferredLanguage,createdDateTime' ` -ErrorAction Stop $script:CachedOrg = if ($OrgResp.value) { $OrgResp.value[0] } else { $OrgResp } Write-Host " [OK] Organisation: $($script:CachedOrg.displayName)" -ForegroundColor DarkGray } catch { Write-TranscriptLog "Pre-fetch org failed: $($_.Exception.Message)" 'WARNING' 'AUTH' } try { $SkuResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/subscribedSkus?$select=skuPartNumber,capabilityStatus,prepaidUnits,consumedUnits' ` -ErrorAction Stop $script:CachedSkus = if ($SkuResp.value) { $SkuResp.value } else { @() } Write-Host " [OK] Licences: $(@($script:CachedSkus).Count) SKUs" -ForegroundColor DarkGray } catch { Write-TranscriptLog "Pre-fetch SKUs failed: $($_.Exception.Message)" 'WARNING' 'AUTH' } try { $LabelResp = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/security/informationProtection/sensitivityLabels' ` -ErrorAction SilentlyContinue $script:CachedLabels = if ($LabelResp.value) { $LabelResp.value } else { @() } Write-Host " [OK] Sensitivity labels: $(@($script:CachedLabels).Count)" -ForegroundColor DarkGray } catch { $script:CachedLabels = @() Write-TranscriptLog "Pre-fetch labels failed: $($_.Exception.Message)" 'WARNING' 'AUTH' } #endregion #region ── PnP.PowerShell ──────────────────────────────────────────────────── $script:PnPAvailable = $false if (-not (Get-Module -ListAvailable -Name PnP.PowerShell -ErrorAction SilentlyContinue)) { Write-Warning " [!] PnP.PowerShell not installed. Run: Install-Module -Name PnP.PowerShell -Force" return } $ExistingPnP = $null try { $ExistingPnP = Get-PnPConnection -ErrorAction SilentlyContinue } catch { } if ($ExistingPnP) { Write-TranscriptLog "Reusing existing PnP.PowerShell session" 'SUCCESS' 'AUTH' $script:PnPAvailable = $true return } $ConfigClientId = $null try { $ConfigClientId = $script:Options.PnP.ClientId } catch { } $HasClientId = ($ConfigClientId -and "$ConfigClientId".Trim() -ne '') if (-not $HasClientId) { Write-Warning " [!] Options.PnP.ClientId is not set in the report config JSON." Write-Warning " SharePoint tenant settings, site collections, and OneDrive data will be unavailable." Write-PnPConnectionGuidance -TenantAdminUrl $TenantAdminUrl -UserPrincipalName $UserPrincipalName return } $PnPVersion = (Get-Module -ListAvailable -Name PnP.PowerShell | Sort-Object Version -Descending | Select-Object -First 1).Version Write-Host " - Connecting PnP.PowerShell v$PnPVersion to $TenantAdminUrl..." -ForegroundColor Cyan try { Invoke-WithRetry -ScriptBlock { Connect-PnPOnline -Url $TenantAdminUrl -ClientId $ConfigClientId ` -Interactive -ErrorAction Stop } -OperationName 'Connect to SharePoint Online (PnP)' $script:PnPAvailable = $true Write-Host " - PnP.PowerShell connected." -ForegroundColor Green Write-TranscriptLog "PnP connected (ClientId: $ConfigClientId)" 'SUCCESS' 'AUTH' } catch { Write-Warning " [!] PnP connection failed: $($_.Exception.Message)" Write-Warning " Continuing in Graph-only mode." Write-PnPConnectionGuidance -TenantAdminUrl $TenantAdminUrl -UserPrincipalName $UserPrincipalName $script:PnPAvailable = $false } #endregion Write-Host " - Connection setup complete." -ForegroundColor Green Write-TranscriptLog "Connection complete for: $UserPrincipalName" 'SUCCESS' 'AUTH' } function Write-PnPConnectionGuidance { [CmdletBinding()] param( [string]$TenantAdminUrl = '', [string]$UserPrincipalName = '' ) $TenantPrefix = '' try { $TenantPrefix = ($TenantAdminUrl -replace 'https://', '' -replace '-admin\.sharepoint\.com.*', '') } catch { } $TenantFqdn = if ($TenantPrefix) { "$TenantPrefix.onmicrosoft.com" } else { 'contoso.onmicrosoft.com' } $AppName = "AsBuiltReport.SharePoint$(if ($TenantPrefix) { ".$TenantPrefix" } else { '' })" Write-Host "" Write-Host " ┌──────────────────────────────────────────────────────────────────────┐" -ForegroundColor Yellow Write-Host " │ HOW TO SET UP: PnP App Registration │" -ForegroundColor Yellow Write-Host " └──────────────────────────────────────────────────────────────────────┘" -ForegroundColor Yellow Write-Host "" Write-Host " Run once to register an app and get a Client ID:" -ForegroundColor White Write-Host "" Write-Host " `$app = Register-PnPEntraIDAppForInteractiveLogin \`" -ForegroundColor Green Write-Host " -ApplicationName '$AppName' \`" -ForegroundColor Green Write-Host " -Tenant '$TenantFqdn' \`" -ForegroundColor Green Write-Host " -SharePointDelegatePermissions 'AllSites.Read','TermStore.Read.All','User.ReadBasic.All'" -ForegroundColor Green Write-Host " `$app.AzureAppId # copy this into JSON config Options.PnP.ClientId" -ForegroundColor Green Write-Host "" Write-Host " Then add to AsBuiltReport.Microsoft.SharePoint.json:" -ForegroundColor White Write-Host ' "PnP": { "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }' -ForegroundColor Green Write-Host "" } function Disconnect-SharePointSession { [CmdletBinding()] param() Write-TranscriptLog "Disconnecting SharePoint sessions" 'INFO' 'AUTH' try { $GraphCtx = Get-MgContext -ErrorAction SilentlyContinue if ($GraphCtx) { Write-Host " - Disconnecting Microsoft Graph..." $null = Disconnect-MgGraph -ErrorAction SilentlyContinue Write-TranscriptLog "Microsoft Graph disconnected" 'SUCCESS' 'AUTH' } } catch { Write-TranscriptLog "Graph disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH' } if ($script:PnPAvailable) { try { Write-Host " - Disconnecting PnP.PowerShell..." $null = Disconnect-PnPOnline -ErrorAction SilentlyContinue Write-TranscriptLog "PnP disconnected" 'SUCCESS' 'AUTH' } catch { Write-TranscriptLog "PnP disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH' } } Write-Host " - Sessions disconnected." -ForegroundColor Green } |