Public/Add-SiSAccessPackageAssignment.ps1
|
function Add-SiSAccessPackageAssignment { <# .SYNOPSIS Assigns one or more users to an Access Package, sourcing the users from an Excel file, a Graph $filter query, a list of UPNs or the pipeline - all through the same flow. .DESCRIPTION Validates the Access Package and Assignment Policy, resolves the target users (batching UPN -> id lookups), shows a preview, asks for confirmation once, and submits the requests via Graph JSON batching (up to 20 per HTTP call), with retry and pacing. Users who already have the assignment are reported as AlreadyAssigned - Graph rejects the duplicate with its own error code (409 InvalidRequestExistingGrant), so there's no separate check beforehand. A request already pending for a user is reported as OpenRequestExists. The folder for the Excel report is chosen first - with -OutputPath, or in a dialog - so the run never stops halfway to ask. -SkipReport skips the report. The results are always returned, one object per user. .PARAMETER AccessPackageId The ObjectId of the Access Package to target. .PARAMETER AssignmentPolicyId The ObjectId of the Assignment Policy within the Access Package. .PARAMETER ExcelPath Path to an Excel file with a 'userPrincipalName' column. (ParameterSet 'Excel') .PARAMETER Filter An OData filter expression evaluated against /v1.0/users. (ParameterSet 'Filter') .PARAMETER UserPrincipalName One or more UPNs directly. (ParameterSet 'Users', default) Also accepts pipeline input: UPN strings, or objects with a userPrincipalName property. Everything piped in is collected first and run as one batch, with one preview and one confirmation. Duplicates are removed. .PARAMETER MaxUsers Safety cap on how many users a single run may touch. Default 500. .PARAMETER WhatIf Resolves and displays the target users, then stops without submitting anything. Returns one result per user, with Status 'Preview - <action>'. .PARAMETER Confirm Asks before submitting (the default). -Confirm:$false submits without asking - for unattended runs, e.g. in an Azure Function. .PARAMETER SkipReport No Excel report - and no folder to choose. The results are still returned as objects. .PARAMETER OutputPath Folder for the Excel report. Without it, a folder dialog opens at the start of the run (Windows). .PARAMETER BatchSize Requests per Graph $batch call. Default and max 20 (Graph's own limit). .PARAMETER DelayMs Pause between batch calls when submitting the requests. Default 1000 ms, which keeps a run at roughly half of Graph's write limit for the app (3000 per 2.5 min). Raised automatically if Graph reports it's close to the limit. .EXAMPLE # Single user, no report Add-SiSAccessPackageAssignment -AccessPackageId $apId -AssignmentPolicyId $polId ` -UserPrincipalName "anna@contoso.com" -SkipReport .EXAMPLE # From the pipeline - a variable or any command that outputs UPNs $users | Add-SiSAccessPackageAssignment -AccessPackageId $apId -AssignmentPolicyId $polId .EXAMPLE # Everyone matching a filter, preview first Add-SiSAccessPackageAssignment -AccessPackageId $apId -AssignmentPolicyId $polId ` -Filter "userType eq 'Member' and employeeType eq 'employee'" -WhatIf .EXAMPLE # Unattended: no dialog, no confirmation, and only the failures $users | Add-SiSAccessPackageAssignment -AccessPackageId $apId -AssignmentPolicyId $polId ` -OutputPath "C:\Reports" -Confirm:$false | Where-Object Status -like 'Failed*' .OUTPUTS SiSGovernance.AssignmentResult, one per user: UserPrincipalName, DisplayName, ObjectId, Status (Submitted / AlreadyAssigned / OpenRequestExists / Failed / Failed - User not found / Preview - Add), Error. .NOTES Required scopes: User.Read.All, EntitlementManagement.ReadWrite.All Least privileged role: Access package assignment manager on the catalog (via PIM) #> [OutputType('SiSGovernance.AssignmentResult')] # The confirmation is asked once for the whole run, in Invoke-AccessPackageAssignmentRequest - # -WhatIf and -Confirm reach it through the preference variables [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called in Invoke-AccessPackageAssignmentRequest, which this function calls')] [CmdletBinding(DefaultParameterSetName = 'Users', SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true)] [guid]$AccessPackageId, [Parameter(Mandatory = $true)] [guid]$AssignmentPolicyId, [Parameter(Mandatory = $true, ParameterSetName = 'Excel')] [string]$ExcelPath, [Parameter(Mandatory = $true, ParameterSetName = 'Filter')] [string]$Filter, [Parameter(Mandatory = $true, ParameterSetName = 'Users', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]]$UserPrincipalName, [Parameter(Mandatory = $false)] [int]$MaxUsers = 500, [Parameter(Mandatory = $false)] [switch]$SkipReport, [Parameter(Mandatory = $false)] [string]$OutputPath, [Parameter(Mandatory = $false)] [ValidateRange(1, 20)] [int]$BatchSize = 20, [Parameter(Mandatory = $false)] [ValidateRange(0, 60000)] [int]$DelayMs = 1000 ) begin { $collectedUpns = [System.Collections.Generic.List[string]]::new() } process { # Pipeline input arrives one item at a time - collect everything first, # so the preview, -MaxUsers, the confirmation and the batching happen once if ($PSCmdlet.ParameterSetName -ne 'Users') { return } foreach ($upn in $UserPrincipalName) { if ("$upn".Trim()) { $collectedUpns.Add("$upn".Trim()) } } } end { $params = @{} foreach ($key in $PSBoundParameters.Keys) { $params[$key] = $PSBoundParameters[$key] } $params['AccessPackageId'] = $AccessPackageId.ToString('D') $params['AssignmentPolicyId'] = $AssignmentPolicyId.ToString('D') if ($PSCmdlet.ParameterSetName -eq 'Users') { if ($collectedUpns.Count -eq 0) { Write-Warning "No users received. Nothing to do." return } $params['UserPrincipalName'] = @($collectedUpns) } Invoke-AccessPackageAssignmentRequest @params -AdminAdd } } |