Public/Add-KritOneDriveShareLinkRecipients.ps1
|
function Add-KritOneDriveShareLinkRecipients { <# .SYNOPSIS Grant additional named recipients access to an existing OneDrive-synced item WITHOUT disrupting any other permissions already in force on it. .DESCRIPTION Calls Microsoft Graph `POST /me/drive/items/{id}/invite` to add one or more recipients (internal users OR external email addresses) to the existing share permission set. The existing permissions (e.g. an earlier `users`-scope share already granted to Josh + Ben) stay untouched — this cmdlet only ADDS. Use cases: • Add a customer contact (e.g. Lincoln) to a proposal-pack folder already shared internally to your delivery team. • Quickly grant a new internal reviewer view-access to an existing share. • Send a Graph-tracked invitation email with a custom message. .PARAMETER LocalPath Full path to a file or folder under the OneDrive for Business sync root. .PARAMETER Recipients Email-address array. External emails work too (per SharePoint external-sharing policy). .PARAMETER Role view (default) | edit. Maps to Graph `roles = ['read']` or `['write']`. .PARAMETER RequireSignIn When set, recipients must sign in (Microsoft / guest account) to access. Default $true — Graph's documented secure default. Pass `-RequireSignIn:$false` for an anonymous-style invite gated by their email (less secure; not recommended for customer-facing customer-engagement packs). .PARAMETER SendInvitation When set, Graph sends an invitation email to each recipient with the share URL. Default $false — most operators send their own cover email out of band so the recipient sees Kritical-branded copy + the deliberate engagement narrative, not Microsoft's transactional template. .PARAMETER Message Optional custom message body included in the invitation email when -SendInvitation is set. Ignored when -SendInvitation is not set. .PARAMETER ExpirationDateTime Optional ISO 8601 expiry on the new permission (e.g. '2026-07-31T17:00:00Z'). .PARAMETER Password Optional password gate (anonymous-style invite only). .PARAMETER UseDeviceCode Force device-code auth flow for headless contexts. .EXAMPLE Add-KritOneDriveShareLinkRecipients -LocalPath 'C:/Users/joshl/OneDrive - Kritical Pty Ltd/EES/EES-proposal-pack-FINAL-SHARED' -Recipients 'lincoln@eeservices.io' -Role view Adds Lincoln (view-access) to the EES share. Josh + Ben's existing edit permission stays exactly as it was. .EXAMPLE Add-KritOneDriveShareLinkRecipients -LocalPath $f -Recipients @('lincoln@eeservices.io','mark@eeservices.io') -Role view -SendInvitation -Message 'EES proposal pack — link arrives separately from joshua.finley@kritical.net.' Adds two external recipients with a Graph-sent invitation email + custom message. .EXAMPLE Add-KritOneDriveShareLinkRecipients -LocalPath $f -Recipients 'reviewer@external.com' -Role view -ExpirationDateTime '2026-07-31T17:00:00Z' -RequireSignIn:$false Adds an external reviewer with a hard expiry; sign-in not required. .OUTPUTS PSCustomObject[] — one row per newly-granted permission: PermissionId [string] Roles [string[]] GrantedToEmails [string[]] GrantedToNames [string[]] WebUrl [string] (when link-style) CreatedAt [string] ISO 8601 .NOTES CONTRACT inputs: - LocalPath : path; must exist + be under OneDrive sync root - Recipients : email[]; required - Role : view|edit (default view) - RequireSignIn : switch (default ON via param-default-true pattern) - SendInvitation : switch (default OFF) - Message : optional string - ExpirationDateTime: optional ISO 8601 - Password : optional outputs: - PSCustomObject[] one per newly-granted permission sideEffects: - Connects to Microsoft Graph (Files.ReadWrite.All + Sites.ReadWrite.All) - Creates new share permissions on the target DriveItem - Optionally sends invitation email (when -SendInvitation) - Does NOT modify or remove existing permissions invariants: - Existing permissions on the item are untouched - Throws on path-outside-sync-root, item-not-found, auth-failure - asserts: paired tests/Unit/OneDriveShareLinkPermissions.Tests.ps1 Author: Joshua Finley Repo: Krit.OmniFramework Added: v1.1.13 — Krit.OmniFramework 2026-06-28 (.1507ab) #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','Password', Justification = 'Microsoft Graph /invite contract requires a plaintext password field on the request body; 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, [Parameter(Mandatory)][string[]]$Recipients, [ValidateSet('view','edit')][string]$Role = 'view', # NOTE: bool (not switch) so the secure default ($true) is honoured without # tripping PSScriptAnalyzer's PSAvoidDefaultValueSwitchParameter rule. Pass # -RequireSignIn:$false explicitly to grant external-email-only access. [bool]$RequireSignIn = $true, [switch]$SendInvitation, [string]$Message, [string]$ExpirationDateTime, [string]$Password, [switch]$UseDeviceCode ) $scopes = @('Files.ReadWrite.All','Sites.ReadWrite.All','User.Read') $resolved = Resolve-KritOneDriveDriveItem -LocalPath $LocalPath -Scopes $scopes -UseDeviceCode:$UseDeviceCode $roleString = if ($Role -eq 'edit') { 'write' } else { 'read' } $body = @{ recipients = @($Recipients | ForEach-Object { @{ email = $_ } }) roles = @($roleString) requireSignIn = [bool]$RequireSignIn sendInvitation = [bool]$SendInvitation } if ($SendInvitation -and $Message) { $body.message = $Message } if ($ExpirationDateTime) { $body.expirationDateTime = $ExpirationDateTime } if ($Password) { $body.password = $Password } $inviteUri = "/v1.0/me/drive/items/$($resolved.ItemId)/invite" $bodyJson = $body | ConvertTo-Json -Depth 5 -Compress $action = "Add $($Recipients -join ', ') as $Role to $($resolved.ItemName)" if (-not $PSCmdlet.ShouldProcess($resolved.ItemName, $action)) { return } Write-Verbose "Inviting $($Recipients.Count) recipient(s) as $Role (requireSignIn=$RequireSignIn, sendInvitation=$SendInvitation)" $resp = Invoke-MgGraphRequest -Method POST -Uri $inviteUri -Body $bodyJson -ContentType 'application/json' -ErrorAction Stop $respValue = Get-KritGraphProp -Object $resp -Name 'value' foreach ($p in @($respValue)) { $emails = @() $names = @() $idV2 = Get-KritGraphProp -Object $p -Name 'grantedToIdentitiesV2' $gV2 = Get-KritGraphProp -Object $p -Name 'grantedToV2' foreach ($g in @($idV2) + @($gV2)) { if ($null -eq $g) { continue } $u = Get-KritGraphProp -Object $g -Name 'user' if ($null -eq $u) { continue } $em = Get-KritGraphProp -Object $u -Name 'email' $dn = Get-KritGraphProp -Object $u -Name 'displayName' if ($em) { $emails += $em } if ($dn) { $names += $dn } } $link = Get-KritGraphProp -Object $p -Name 'link' [pscustomobject]@{ PermissionId = Get-KritGraphProp -Object $p -Name 'id' Roles = @(Get-KritGraphProp -Object $p -Name 'roles') GrantedToEmails = $emails GrantedToNames = $names WebUrl = if ($link) { Get-KritGraphProp -Object $link -Name 'webUrl' } else { $null } CreatedAt = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssZ') } } } |