Public/Grant-IOManagedIdentityPermission.ps1
|
function Grant-IOManagedIdentityPermission { <# .SYNOPSIS Grants Microsoft Graph (or other API) app role permissions to a managed identity. .DESCRIPTION This is one of the most common pain points — assigning API permissions to a managed identity requires raw Graph API calls. This command simplifies it. .EXAMPLE Grant-IOManagedIdentityPermission -ManagedIdentityName "my-func-app" -Permission "Mail.Send" .EXAMPLE Grant-IOManagedIdentityPermission -ManagedIdentityObjectId "aaaa-bbbb..." -Permission "User.Read.All","Group.Read.All" .EXAMPLE Grant-IOManagedIdentityPermission -ManagedIdentityName "my-vm" -Permission "Sites.Read.All" -ResourceAppId "00000003-0000-0ff1-ce00-000000000000" # Grants SharePoint Online permission instead of MS Graph #> [CmdletBinding(DefaultParameterSetName = 'ByName', SupportsShouldProcess)] param( [Parameter(ParameterSetName = 'ByName', Mandatory)] [ValidateNotNullOrEmpty()] [string]$ManagedIdentityName, [Parameter(ParameterSetName = 'ById', Mandatory)] [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] [string]$ManagedIdentityObjectId, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$Permission, [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] [string]$ResourceAppId = $script:MsGraphAppId, [string]$ToCsv ) $cmdName = $MyInvocation.MyCommand.Name # ── Resolve managed identity service principal ───────────────────────── if ($PSCmdlet.ParameterSetName -eq 'ByName') { Write-IOLog "Looking up managed identity: $ManagedIdentityName" -Level Verbose -Component $cmdName # Sanitize input for OData filter — block injection characters if ($ManagedIdentityName -match '[;$&\\/<>{}]') { throw [System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("ManagedIdentityName contains invalid characters."), 'IO_InvalidInput', [System.Management.Automation.ErrorCategory]::InvalidArgument, $ManagedIdentityName ) } $sanitizedName = $ManagedIdentityName -replace "'", "''" $filter = "displayName eq '$sanitizedName'" $sps = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals?`$filter=$filter&`$select=id,displayName,appId,servicePrincipalType" if (-not $sps -or $sps.Count -eq 0) { throw [System.Management.Automation.ErrorRecord]::new( [System.Management.Automation.ItemNotFoundException]::new( "Managed identity '$ManagedIdentityName' not found. Verify the name matches the enterprise app display name." ), 'IO_ManagedIdentityNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $ManagedIdentityName ) } if ($sps.Count -gt 1) { Write-IOLog "Multiple matches found for '$ManagedIdentityName'. Using first result: $($sps[0].id)" -Level Warning -Component $cmdName } $miSp = $sps[0] } else { Write-IOLog "Looking up managed identity by ObjectId: $ManagedIdentityObjectId" -Level Verbose -Component $cmdName $miSp = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals/$ManagedIdentityObjectId" -SingleResult -NoPagination if (-not $miSp -or $miSp.Count -eq 0) { throw [System.Management.Automation.ErrorRecord]::new( [System.Management.Automation.ItemNotFoundException]::new( "Service principal with ObjectId '$ManagedIdentityObjectId' not found." ), 'IO_ServicePrincipalNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $ManagedIdentityObjectId ) } $miSp = $miSp[0] } # ── Resolve resource service principal ───────────────────────────────── Write-IOLog "Resolving resource app: $ResourceAppId" -Level Verbose -Component $cmdName $resSps = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals?`$filter=appId eq '$ResourceAppId'&`$select=id,displayName,appRoles" if (-not $resSps -or $resSps.Count -eq 0) { throw [System.Management.Automation.ErrorRecord]::new( [System.Management.Automation.ItemNotFoundException]::new( "Resource application with appId '$ResourceAppId' not found in tenant." ), 'IO_ResourceAppNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $ResourceAppId ) } $resourceSp = $resSps[0] # ── Get existing assignments to avoid duplicates ─────────────────────── $existingAssignments = Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals/$($miSp.id)/appRoleAssignments?`$select=appRoleId,resourceId" $existingRoleIds = @($existingAssignments | ForEach-Object { $_.appRoleId }) # ── Assign each permission ───────────────────────────────────────────── $results = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($perm in $Permission) { $appRole = $resourceSp.appRoles | Where-Object { $_.value -eq $perm -and $_.allowedMemberTypes -contains 'Application' } if (-not $appRole) { Write-IOLog "Permission '$perm' not found as an Application role on resource '$($resourceSp.displayName)'. Skipped." -Level Warning -Component $cmdName $results.Add([PSCustomObject]@{ ManagedIdentity = $miSp.displayName MIObjectId = $miSp.id Permission = $perm ResourceApp = $resourceSp.displayName Status = 'NOT_FOUND' Message = 'App role not found on resource' }) continue } if ($appRole.id -in $existingRoleIds) { Write-IOLog "Permission '$perm' already assigned to '$($miSp.displayName)'. Skipped." -Level Info -Component $cmdName $results.Add([PSCustomObject]@{ ManagedIdentity = $miSp.displayName MIObjectId = $miSp.id Permission = $perm ResourceApp = $resourceSp.displayName Status = 'ALREADY_ASSIGNED' Message = 'Permission was already granted' }) continue } if ($PSCmdlet.ShouldProcess("$($miSp.displayName)", "Grant '$perm' from $($resourceSp.displayName)")) { try { $body = @{ principalId = $miSp.id resourceId = $resourceSp.id appRoleId = $appRole.id } Invoke-IOGraphRequest -Uri "v1.0/servicePrincipals/$($miSp.id)/appRoleAssignments" -Method POST -Body $body -NoPagination | Out-Null Write-IOLog "Granted '$perm' to '$($miSp.displayName)'." -Level Info -Component $cmdName $results.Add([PSCustomObject]@{ ManagedIdentity = $miSp.displayName MIObjectId = $miSp.id Permission = $perm ResourceApp = $resourceSp.displayName Status = 'GRANTED' Message = 'Successfully assigned' }) } catch { Write-IOLog "Failed to grant '$perm': $($_.Exception.Message)" -Level Warning -Component $cmdName $results.Add([PSCustomObject]@{ ManagedIdentity = $miSp.displayName MIObjectId = $miSp.id Permission = $perm ResourceApp = $resourceSp.displayName Status = 'FAILED' Message = $_.Exception.Message }) } } } Export-IOResult -Data $results -ToCsv $ToCsv -CommandName $cmdName } |