Public/New-KritOneDriveShareLink.ps1
|
function New-KritOneDriveShareLink { <# .SYNOPSIS Generate a Microsoft Graph OneDrive sharing link for a local OneDrive-synced file or folder and return the URL + metadata as a PSCustomObject. .DESCRIPTION Uses user-delegated Microsoft Graph auth (Files.ReadWrite.All + Sites.ReadWrite.All + User.Read scopes) to mint a `createLink` permission on a DriveItem in the operator's personal OneDrive for Business drive. Maps the local OneDrive sync path to the cloud DriveItem by reading the sync root from HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1 and the path-relative `/me/drive/root:/<path>` Graph endpoint. Use cases: • Send a customer-engagement folder to internal reviewers (Ben / Josh / etc.) without 10 MB email attachments. • Generate time-limited / password-protected anonymous links for external sharing. • Build customer-folder share-links into engagement-letter / SOW generation pipelines. Auth flow: • Default: interactive browser via WAM (needs a window handle — works from a normal PowerShell window, not from an embedded terminal subshell). • Fallback: device-code (set -UseDeviceCode) for headless contexts. • First-run consent only — token cached per CurrentUser scope after that. Security posture: Microsoft Graph user-delegated auth using the operator's M365 identity. No API keys. No service-principal client secrets in scripts. The operator signs in with their M365 account; Graph caches the delegated token via the standard Microsoft.Graph.Authentication module. .PARAMETER LocalPath Full path to a file or folder under the OneDrive for Business sync root. Must exist locally. Will be resolved + validated against the registered sync root. .PARAMETER ShareType view (default) | edit. Maps to the Graph `link.type` value. .PARAMETER ShareScope organization (default) | anonymous | users. Maps to the Graph `link.scope` value. anonymous — anyone with the link can access (use Password / ExpirationDateTime to tighten); useful for one-off external shares. organization — anyone in the operator's tenant who's signed in (default; safest internal-only share). users — explicit named-recipient access; pair with -Recipients. Most-restrictive. .PARAMETER Recipients Email-address array. REQUIRED when ShareScope = users. Ignored otherwise. .PARAMETER Password Optional password gate for anonymous links. Only honoured by Graph when ShareScope = anonymous. .PARAMETER ExpirationDateTime Optional ISO 8601 expiry. Only honoured by Graph when ShareScope = anonymous. .PARAMETER UseDeviceCode Force device-code auth flow (browser fallback). Use this in headless / embedded-terminal contexts where WAM cannot acquire a window handle. .EXAMPLE $share = New-KritOneDriveShareLink -LocalPath 'C:/Users/joshl/OneDrive - Kritical Pty Ltd/EES/EES-proposal-pack-FINAL-SHARED' $share.WebUrl # https://kriticalptyltd-my.sharepoint.com/:f:/g/personal/joshua_finley_kritical_net/... Generates a tenant-scoped view link for the EES proposal pack folder. .EXAMPLE $share = New-KritOneDriveShareLink -LocalPath 'C:/Users/joshl/OneDrive - Kritical Pty Ltd/EES/folder' -ShareType edit -ShareScope users -Recipients @('ben.szypowski@kritical.net','joshua.finley@kritical.net') Generates an edit-scope named-recipient link for Ben + Josh. .EXAMPLE $share = New-KritOneDriveShareLink -LocalPath 'C:/Users/joshl/OneDrive - Kritical Pty Ltd/Customer/folder' -ShareScope anonymous -Password 'OneTime-2026' -ExpirationDateTime '2026-07-31T17:00:00Z' Generates a password-protected anonymous link expiring 31 Jul 2026 — for one-off external sharing. .OUTPUTS PSCustomObject with these properties: WebUrl [string] — the sharing URL to send ShareId [string] — Graph share-permission ID (use for revocation) ItemId [string] — DriveItem ID DriveId [string] — parent Drive ID ItemName [string] — file/folder name IsFolder [bool] ShareType [string] — view|edit (echoed back) ShareScope [string] — anonymous|organization|users (echoed back) CreatedAt [string] — ISO 8601 timestamp of link creation .NOTES CONTRACT inputs: - LocalPath : path; must exist + be under OneDrive sync root - ShareType : view|edit - ShareScope : anonymous|organization|users - Recipients : email[] (required when ShareScope=users) - Password : optional (anonymous only) - ExpirationDateTime: optional ISO 8601 (anonymous only) outputs: - PSCustomObject with WebUrl + ShareId + ItemId + DriveId + ItemName + IsFolder + ShareType + ShareScope + CreatedAt sideEffects: - Connects to Microsoft Graph (delegated; cached per CurrentUser) - Creates a sharing-link permission on the target DriveItem (visible in OneDrive admin) - Does NOT modify the item bytes - Does NOT send the link itself (caller wires into email / message) invariants: - Returns WebUrl only after the createLink Graph response is received - Throws on path-outside-sync-root, item-not-found, auth-failure - asserts: paired tests/Unit/OneDriveShareLink.Tests.ps1 Author: Joshua Finley Repo: Krit.OmniFramework Promoted-from: Kritical-M365DSC/management/New-KritOneDriveShareLink.ps1 (2026-06-28) #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','Password', Justification = 'Microsoft Graph /createLink contract requires a plaintext password field on the request body (only honoured for anonymous-scope links); converting to SecureString would force an unwrap that defeats the protection. Operator-supplied password lives in memory for the single call only.')] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string]$LocalPath, [ValidateSet('view','edit')] [string]$ShareType = 'view', [ValidateSet('anonymous','organization','users')] [string]$ShareScope = 'organization', [string[]]$Recipients, [string]$Password, [string]$ExpirationDateTime, [switch]$UseDeviceCode ) # --- 1. Ensure Microsoft Graph Authentication module present (Invoke-MgGraphRequest is all we need; we use raw REST below) --- if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) { Write-Verbose "Installing Microsoft.Graph.Authentication ..." Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop } Import-Module Microsoft.Graph.Authentication -ErrorAction Stop # --- 2. Resolve local path → OneDrive cloud-relative path --- $resolvedLocal = (Resolve-Path -LiteralPath $LocalPath -ErrorAction Stop).Path $odBusinessRoot = $null try { $odBusinessRoot = (Get-ItemProperty -Path 'HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1' -Name UserFolder -ErrorAction Stop).UserFolder } catch { throw "OneDrive for Business sync root not found in registry HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1\UserFolder — verify OneDrive sync client signed in." } $resolvedLocal = $resolvedLocal -replace '\\','/' -replace '/$','' $odBusinessRoot = $odBusinessRoot -replace '\\','/' -replace '/$','' if (-not $resolvedLocal.StartsWith($odBusinessRoot, [StringComparison]::OrdinalIgnoreCase)) { throw "Path '$resolvedLocal' is not under the OneDrive sync root '$odBusinessRoot' — only OneDrive-synced files can be shared." } $relativePath = $resolvedLocal.Substring($odBusinessRoot.Length).TrimStart('/') Write-Verbose "Local path: $resolvedLocal" Write-Verbose "OneDrive root: $odBusinessRoot" Write-Verbose "Cloud-relative: $relativePath" # --- 3. Connect to Microsoft Graph (delegated; cached token re-used if scopes match) --- $graphScopes = @('Files.ReadWrite.All','Sites.ReadWrite.All','User.Read') $ctx = Get-MgContext $needConnect = $true if ($ctx -and $ctx.Account) { $missingScope = $graphScopes | Where-Object { $ctx.Scopes -notcontains $_ } if (-not $missingScope) { Write-Verbose "Already connected as $($ctx.Account)" $needConnect = $false } } if ($needConnect) { Write-Verbose "Connecting to Microsoft Graph (delegated, scopes: $($graphScopes -join ', '))" if ($UseDeviceCode) { Connect-MgGraph -Scopes $graphScopes -UseDeviceCode -NoWelcome -ErrorAction Stop } else { try { Connect-MgGraph -Scopes $graphScopes -NoWelcome -ErrorAction Stop } catch { if ($_.Exception.Message -match 'window handle') { Write-Verbose "Browser flow needs a window handle; falling back to device code." Connect-MgGraph -Scopes $graphScopes -UseDeviceCode -NoWelcome -ErrorAction Stop } else { throw } } } } # --- 4. Look up DriveItem by path --- $encodedPath = ($relativePath -split '/' | ForEach-Object { [uri]::EscapeDataString($_) }) -join '/' $itemUri = "/v1.0/me/drive/root:/$encodedPath" Write-Verbose "Looking up: $itemUri" $item = Invoke-MgGraphRequest -Method GET -Uri $itemUri -ErrorAction Stop Write-Verbose "Found DriveItem: $($item.id) — $($item.name) — $(if ($item.folder) {'(folder)'} else {'(file)'})" # --- 5. Create the sharing link --- $linkBody = @{ type = $ShareType scope = $ShareScope } if ($Password) { $linkBody.password = $Password } if ($ExpirationDateTime) { $linkBody.expirationDateTime = $ExpirationDateTime } if ($Recipients -and $ShareScope -eq 'users') { $linkBody.recipients = @($Recipients | ForEach-Object { @{ email = $_ } }) } $createUri = "/v1.0/me/drive/items/$($item.id)/createLink" $linkBodyJson = $linkBody | ConvertTo-Json -Depth 5 -Compress Write-Verbose "Creating $ShareType / $ShareScope link" $linkResp = Invoke-MgGraphRequest -Method POST -Uri $createUri -Body $linkBodyJson -ContentType 'application/json' -ErrorAction Stop [pscustomobject]@{ WebUrl = $linkResp.link.webUrl ShareId = $linkResp.id ItemId = $item.id DriveId = $item.parentReference.driveId ItemName = $item.name IsFolder = [bool]$item.folder ShareType = $ShareType ShareScope = $ShareScope CreatedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssZ') } } |