Functions/Public/Add-MsGServicePrincipalAPIPermission.ps1

function Add-MsGServicePrincipalAPIPermission {
    <#
    .SYNOPSIS
    Adds specified API permission to service principal.
    .DESCRIPTION
    This function adds specified API permissions to an service principal. Both application and delegated permissions can be assigned for any resource application.
    .PARAMETER DisplayName
    Display Name of the service principal.
    .PARAMETER AppId
    App/Client Id of the service principal.
    .PARAMETER ObjectId
    Object Id of the service principal.
    .PARAMETER Permission
    List of permissions to add to the service principal.
    .PARAMETER Type
    Type of permissions to add to the service principal.
    .PARAMETER ResourceAppId
    App/Client Id of the resource application that the permissions are assigned from.
    .PARAMETER Force
    Bypasses the confirmation check.
    .PARAMETER Headers
    Authentication Headers to connect to Microsoft Graph.
    .PARAMETER Jwt
    Jwt to connect to Microsoft Graph.
    .EXAMPLE
    PS> Add-MsGServicePrincipalAPIPermission -DisplayName TestApp-01 -Permission User.Read.All -Type Application -AdminConsent -Force

    This adds the User.Read.All Application API Permission for Microsoft Graph to the TestApp-01 service principal, allowing it to read user data in the tenant.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Name')]
    param(
        [parameter(Mandatory = $true, ParameterSetName = 'Name', Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)][string]$DisplayName,
        [parameter(Mandatory = $true, ParameterSetName = 'AppId', Position = 1)][string]$AppId,
        [parameter(Mandatory = $true, ParameterSetName = 'ObjId', Position = 2)][string]$ObjectId,
        [parameter(Mandatory = $true, Position = 3)][string[]]$Permission,
        [parameter(Mandatory = $false, Position = 4)][ValidateSet('Application', 'Delegated')][string]$Type = 'Application',
        [parameter(Mandatory = $false)][string]$ResourceAppId = '00000003-0000-0000-c000-000000000000',
        [parameter(Mandatory = $false)][switch]$Force,
        [parameter(Mandatory = $false)][hashtable]$Headers,
        [parameter(Mandatory = $false)][string]$Jwt
    )
    BEGIN {
        # Session Checks
        if ([string]::IsNullOrEmpty($Headers) -and [string]::IsNullOrEmpty($Jwt)) {
            if ($null -ne $myGraphToken) {
                Write-Verbose "Setting Jwt to Global: $myGraphToken"
                $Jwt = $myGraphToken
            }
        }
        if ([string]::IsNullOrEmpty($Headers) -and -not([string]::IsNullOrEmpty($Jwt))) {
            $Headers = @{
                'Authorization' = 'Bearer {0}' -f $Jwt
                'Content-Type'  = 'application/json'
            }
        }
        if ([string]::IsNullOrEmpty($Headers)) {
            $errorMessage = '{0} : Unable to run command. Headers are empty.' -f $MyInvocation.MyCommand.Name
            throw $errorMessage
        }
        # end Session Checks

        # Retrieve Resource Permissions
        try {
            $resourceApp = Get-MsGServicePrincipal -AppId $ResourceAppId -Headers $Headers -ErrorAction 'Stop'
            $applicationPermissionTable = $resourceApp | Select-Object -ExpandProperty 'appRoles' | Group-Object 'value' -AsHashTable -AsString
            $delegatedPermissionTable = $resourceApp | Select-Object -ExpandProperty 'oauth2PermissionScopes' | Group-Object 'value' -AsHashTable -AsString
        }
        catch {
            $errorMessage = Get-MsGErrorMessage $_
            $errorException = '{0} : Unable to retrieve resource tables. Error: {1}' -f $MyInvocation.MyCommand.Name, $errorMessage
            throw $errorException
        }
        # end region
    }
    PROCESS {
        # Retrieve Application from Entra ID
        try {
            switch ($PSCmdlet.ParameterSetName) {
                "Name" {
                    $spEndpoint = "servicePrincipals?`$filter=displayName eq '{0}'" -f $DisplayName
                }
                "AppId" {
                    $spEndpoint = "servicePrincipals?`$filter=appId eq '{0}'" -f $AppId
                }
                "ObjId" {
                    $spEndpoint = "servicePrincipals/{0}" -f $ObjectId
                }
            }
            $servicePrincipal = Invoke-MsGRequest -Method Get -Endpoint $spEndpoint -Headers $Headers -ErrorAction 'Stop'
            if ([string]::IsNullOrEmpty($servicePrincipal)) {
                $errorMessage = 'Service Principal not found.'
                throw $errorMessage
            }
        }
        catch {
            $errorMessage = Get-MsGErrorMessage $_
            $errorException = '{0} : Unable to retrieve service principal from Entra ID. Error: {1}' -f $MyInvocation.MyCommand.Name, $errorMessage
            throw $errorException
        }
        $appRoleAssignToEndpoint = 'servicePrincipals/{0}/appRoleAssignedTo' -f $servicePrincipal.id
        # end region

        # Apply API Permissions
        switch ($Type) {
            "Application" {
                # Iterate through application permissions.
                foreach ($SpecifiedPermission in $Permission) {
                    $permissionId = $null
                    $permissionBody = $null

                    # Retrieve and validate permission id.
                    $permissionId = $applicationPermissionTable[$SpecifiedPermission].Id
                    if ([string]::IsNullOrEmpty($permissionId)) {
                        $warningMessage = 'Unable to retrieve the id for the {0} permission. This will not be added to the application.' -f $SpecifiedPermission
                        Write-Warning $warningMessage
                        continue
                    }
                    $permissionBody = @{
                        'principalId' = $servicePrincipal.id
                        'resourceId'  = $resourceApp.id
                        'appRoleId'   = $permissionId
                    }

                    # Assign permission to service principal if it exists.
                    if ($Force.IsPresent -or ($PSCmdlet.ShouldContinue("Add the '$SpecifiedPermission' Application permission to '$($servicePrincipal.displayName)'.", "Are you sure you want to proceed?"))) {
                        try {
                            Invoke-MsGRequest -Method Post -Endpoint $appRoleAssignToEndpoint -Body $permissionBody -Headers $Headers -ErrorAction 'Stop'
                        }
                        catch {
                            $errorMessage = Get-MsGErrorMessage $_
                            $warningMessage = 'Unable to assign permission {0} to {1}. Error: {2}' -f $SpecifiedPermission, $servicePrincipal.displayName, $errorMessage
                            Write-Warning $warningMessage
                            continue
                        }
                    }
                }
            }
            "Delegated" {
                # Validate Permission Set
                $validPermissions = New-Object -TypeName System.Collections.Generic.List[string]
                foreach ($SpecifiedPermission in $Permission) {
                    $permissionObj = $null
                    $permissionObj = $delegatedPermissionTable[$SpecifiedPermission]
                    if (-not([string]::IsNullOrEmpty($permissionObj.value))) {
                        $validPermissions.Add($permissionObj.value)
                    }
                }

                # Retrieve current oauth2 permissions for service principal.
                try {
                    $oauth2PermissionsEndpoint = "oauth2PermissionGrants?`$filter=consentType eq 'AllPrincipals' AND resourceId eq '{0}' AND clientId eq '{1}'" -f $resourceApp.id, $servicePrincipal.id
                    $oauth2PermissionGrants = Invoke-MsGRequest -Method Get -Endpoint $oauth2PermissionsEndpoint -Headers $Headers
                }
                catch {
                    $errorMessage = Get-MsGErrorMessage $_
                    $errorException = '{0} : Unable to validate delegated permissions for service principal from Entra ID. Error: {1}' -f $MyInvocation.MyCommand.Name, $errorMessage
                    throw $errorException
                }

                # If none exist, create net new. Otherwise add on to existing permissions
                if ([string]::IsNullOrEmpty($oauth2PermissionGrants)) {
                    # Create new oauth2 permission grants.
                    $scopeString = ($validPermissions | Sort-Object | Select-Object -Unique) -join ' '
                    $oauth2PermissionBody = @{
                        'clientId'    = $servicePrincipal.id
                        'consentType' = 'AllPrincipals'
                        'resourceId'  = $resourceApp.id
                        'scope'       = $scopeString
                    }
                    $endpoint = 'oauth2PermissionGrants'
                    if ($Force.IsPresent -or ($PSCmdlet.ShouldContinue("Consent to the following delegated permissions to the '$($servicePrincipal.displayName)' service principal: $scopeString.", "Are you sure you want to proceed?"))) {
                        try {
                            Invoke-MsGRequest -Method Post -Endpoint $endpoint -Body $oauth2PermissionBody -Headers $Headers -ErrorAction 'Stop'
                        }
                        catch {
                            $errorMessage = Get-MsGErrorMessage $_
                            $errorException = '{0} : Unable to add delegated permissions for service principal from Entra ID. Error: {1}' -f $MyInvocation.MyCommand.Name, $errorMessage
                            throw $errorException
                        }
                    }
                }
                else {
                    # All Principals permission exists for resource. Adding on to existing scope.
                    $oauth2PermissionGrants.scope.Trim().Split(' ') | ForEach-Object { $validPermissions.Add($_) }
                    $scopeString = ($validPermissions | Sort-Object | Select-Object -Unique) -join ' '

                    # Update existing oauth2 permission grants.
                    if ($Force.IsPresent -or ($PSCmdlet.ShouldContinue("Update the scopes for '$($servicePrincipal.displayName)' from '$($oauth2PermissionGrants.scope.Trim())' to '$scopeString'.", "Are you sure you want to proceed?"))) {
                        try {
                            $endpoint = 'oauth2PermissionGrants/{0}' -f $oauth2PermissionGrants.id
                            $body = @{'scope' = $scopeString }
                            Invoke-MsGRequest -Method Patch -Endpoint $endpoint -Body $body -Headers $Headers -ErrorAction 'Stop'
                        }
                        catch {
                            $errorMessage = Get-MsGErrorMessage $_
                            $errorException = '{0} : Unable to update delegated permissions for service principal from Entra ID. Error: {1}' -f $MyInvocation.MyCommand.Name, $errorMessage
                            throw $errorException
                        }
                    }
                }
            }
        }
        # end region
    }
}