Public/New-CIEMAzureManagedIdentity.ps1

function New-CIEMAzureManagedIdentity {
    <#
    .SYNOPSIS
        Configures an Azure Web App's managed identity with all permissions required for CIEM security checks.

    .DESCRIPTION
        Assigns all permissions required to run Devolutions CIEM security checks to an existing Azure Web App's
        system-assigned managed identity. This includes:
        - Microsoft Graph API application permissions (via admin consent)
        - Azure Resource Manager RBAC role assignment (Reader)
        - Key Vault data plane RBAC roles

        The function uses Get-CIEMRequiredPermission to determine the exact permissions needed.

        Unlike New-PSUAzureServicePrincipal, this function does not create credentials - managed identities
        authenticate automatically when running within Azure services.

    .PARAMETER WebAppName
        The name of the Azure Web App whose managed identity will be configured.

    .PARAMETER ResourceGroupName
        The resource group containing the Azure Web App.

    .PARAMETER Scope
        The scope for the ARM RBAC role assignment. Can be a subscription or management group.
        Examples:
        - "/subscriptions/<subscription-id>"
        - "/providers/Microsoft.Management/managementGroups/<mg-name>"

    .PARAMETER EnableSystemAssignedIdentity
        If the web app does not have a system-assigned managed identity enabled, enable it.
        Default: $true

    .PARAMETER SkipRoleAssignment
        Skip the ARM RBAC role assignment. Useful if you want to assign at a different scope later.

    .PARAMETER SkipAdminConsent
        Skip granting admin consent for Graph API permissions. You will need to grant consent manually
        in the Azure Portal.

    .OUTPUTS
        [PSCustomObject] Object containing:
        - WebAppName: The web app name
        - ResourceGroupName: The resource group name
        - PrincipalId: The managed identity's service principal object ID
        - TenantId: The Azure AD tenant ID
        - Permissions: The permissions configured

    .EXAMPLE
        New-CIEMAzureManagedIdentity -WebAppName "devolutions-ciem-psu" -ResourceGroupName "devolutions-ciem-rg" -Scope "/subscriptions/12345678-1234-1234-1234-123456789012"
        # Configures the managed identity for the specified web app

    .EXAMPLE
        New-CIEMAzureManagedIdentity -WebAppName "my-psu-app" -ResourceGroupName "my-rg" -Scope "/providers/Microsoft.Management/managementGroups/my-mg" -SkipAdminConsent
        # Configures at management group scope, skipping admin consent (grant manually later)

    .NOTES
        Required modules:
        - Az.Accounts (for Connect-AzAccount)
        - Az.Resources (for role assignment cmdlets)
        - Az.Websites (for web app cmdlets)
        - Microsoft.Graph.Applications (for Graph API permission grants, unless -SkipAdminConsent)

        Required Azure permissions:
        - Global Administrator or Privileged Role Administrator role (for admin consent)
        - Owner or User Access Administrator role on the target scope (for role assignment)
        - Contributor on the web app (if enabling managed identity)
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [string]$WebAppName,

        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [ValidatePattern('^/(subscriptions/[a-f0-9-]+|providers/Microsoft\.Management/managementGroups/.+)$')]
        [string]$Scope,

        [Parameter()]
        [switch]$EnableSystemAssignedIdentity = $true,

        [Parameter()]
        [switch]$SkipRoleAssignment,

        [Parameter()]
        [switch]$SkipAdminConsent
    )

    $ErrorActionPreference = 'Stop'

    #region Prerequisites Check
    Write-Verbose "Checking prerequisites..."

    # Check Azure connection
    $context = Get-AzContext
    if (-not $context) {
        throw "Not connected to Azure. Run Connect-AzAccount first."
    }

    $tenantId = $context.Tenant.Id
    Write-Verbose "Connected to tenant: $tenantId"
    #endregion

    #region Get Required Permissions
    Write-Verbose "Getting required permissions from CIEM checks..."
    $permissions = Get-CIEMRequiredPermission
    Write-Verbose "Found $($permissions.CheckCount) checks requiring permissions"
    Write-Verbose "Graph permissions: $($permissions.Graph.Count)"
    Write-Verbose "ARM permissions: $($permissions.ARM.Count)"
    Write-Verbose "Key Vault data plane permissions: $($permissions.KeyVaultDataPlane.Count)"
    #endregion

    #region Get Web App and Managed Identity
    Write-Verbose "Getting web app: $WebAppName in resource group: $ResourceGroupName"
    $webApp = Get-AzWebApp -Name $WebAppName -ResourceGroupName $ResourceGroupName -ErrorAction Stop

    if (-not $webApp) {
        throw "Web app '$WebAppName' not found in resource group '$ResourceGroupName'."
    }

    # Check if system-assigned managed identity is enabled
    $principalId = $null
    if ($webApp.Identity) {
        $principalId = $webApp.Identity.PrincipalId
    }

    if (-not $principalId) {
        if ($EnableSystemAssignedIdentity) {
            if ($PSCmdlet.ShouldProcess($WebAppName, "Enable system-assigned managed identity")) {
                Write-Verbose "Enabling system-assigned managed identity on web app..."
                $webApp = Set-AzWebApp -Name $WebAppName -ResourceGroupName $ResourceGroupName -AssignIdentity $true
                $principalId = $webApp.Identity.PrincipalId

                if (-not $principalId) {
                    throw "Failed to enable system-assigned managed identity on web app '$WebAppName'."
                }

                Write-Verbose "System-assigned managed identity enabled. Principal ID: $principalId"

                # Wait for replication
                Write-Verbose "Waiting for Azure AD replication..."
                Start-Sleep -Seconds 15
            }
        }
        else {
            throw "Web app '$WebAppName' does not have a system-assigned managed identity enabled. Use -EnableSystemAssignedIdentity or enable it manually."
        }
    }
    else {
        Write-Verbose "Web app has system-assigned managed identity. Principal ID: $principalId"
    }
    #endregion

    #region Grant Graph API Permissions (Admin Consent)
    if ($permissions.Graph.Count -gt 0 -and -not $SkipAdminConsent) {
        if ($PSCmdlet.ShouldProcess($WebAppName, "Grant admin consent for Microsoft Graph API permissions")) {
            Write-Verbose "Granting admin consent for Microsoft Graph API permissions..."
            Write-Verbose "Getting access token from current Az context..."

            try {
                # Get Graph token from current Az context (admin user or SP with admin permissions)
                $graphToken = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop

                # Handle both string and SecureString token formats (Az.Accounts version differences)
                if ($graphToken.Token -is [System.Security.SecureString]) {
                    $secureToken = $graphToken.Token
                } else {
                    $secureToken = ConvertTo-SecureString $graphToken.Token -AsPlainText -Force
                }

                Connect-MgGraph -AccessToken $secureToken -NoWelcome -ErrorAction Stop

                # Microsoft Graph service principal (well-known app ID)
                $graphAppId = "00000003-0000-0000-c000-000000000000"
                $graphSp = Get-MgServicePrincipal -Filter "appId eq '$graphAppId'"

                # Get the managed identity's service principal
                $miSp = Get-MgServicePrincipal -Filter "id eq '$principalId'"
                if (-not $miSp) {
                    Write-Warning "Could not find service principal for managed identity. Waiting and retrying..."
                    Start-Sleep -Seconds 10
                    $miSp = Get-MgServicePrincipal -Filter "id eq '$principalId'"
                }

                if (-not $miSp) {
                    throw "Could not find service principal for managed identity with ID '$principalId'."
                }

                $grantedCount = 0
                foreach ($permissionName in $permissions.Graph) {
                    $appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq $permissionName }
                    if (-not $appRole) {
                        Write-Warning "Could not find Graph permission: $permissionName"
                        continue
                    }

                    Write-Verbose " Granting consent for: $permissionName"
                    try {
                        New-MgServicePrincipalAppRoleAssignment `
                            -ServicePrincipalId $miSp.Id `
                            -PrincipalId $miSp.Id `
                            -ResourceId $graphSp.Id `
                            -AppRoleId $appRole.Id `
                            -ErrorAction Stop | Out-Null
                        $grantedCount++
                    }
                    catch {
                        if ($_.Exception.Message -like "*already exists*") {
                            Write-Verbose " Permission $permissionName already granted"
                            $grantedCount++
                        }
                        else {
                            Write-Warning "Failed to grant consent for $permissionName : $_"
                        }
                    }
                }

                Write-Verbose "Admin consent granted for $grantedCount Graph API permissions"

                Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
            }
            catch {
                Write-Warning "Failed to grant admin consent: $_"
                Write-Warning "Ensure you are logged in as an admin user (Connect-AzAccount) with permission to grant consent."
                Write-Warning @"
To grant permissions manually via Azure Portal:
1. Go to Azure Portal > Enterprise applications
2. Search for the managed identity by its Principal ID: $principalId
3. Go to Permissions > Grant admin consent
Or use PowerShell with Microsoft.Graph module directly.
"@

            }
        }
    }
    elseif ($permissions.Graph.Count -gt 0) {
        Write-Warning @"
Admin consent was skipped. You must grant admin consent manually:
1. Go to Azure Portal > Enterprise applications
2. Search for the managed identity by its Principal ID: $principalId
3. Go to Permissions > Add a permission > Microsoft Graph > Application permissions
4. Add these permissions:
$($permissions.Graph | ForEach-Object { " - $_" } | Out-String)
5. Grant admin consent

Or use PowerShell:
  Connect-MgGraph -Scopes 'AppRoleAssignment.ReadWrite.All'
  # Then grant app role assignments to the managed identity
"@

    }
    #endregion

    #region Assign Azure RBAC Roles
    if (-not $SkipRoleAssignment -and $permissions.AzureRoles.Count -gt 0) {
        foreach ($roleName in $permissions.AzureRoles) {
            if ($PSCmdlet.ShouldProcess($Scope, "Assign $roleName role to managed identity")) {
                Write-Verbose "Assigning $roleName role at scope: $Scope"

                $existingAssignment = Get-AzRoleAssignment `
                    -ObjectId $principalId `
                    -Scope $Scope `
                    -RoleDefinitionName $roleName `
                    -ErrorAction SilentlyContinue

                if ($existingAssignment) {
                    Write-Verbose "$roleName role already assigned at this scope"
                }
                else {
                    New-AzRoleAssignment `
                        -ObjectId $principalId `
                        -Scope $Scope `
                        -RoleDefinitionName $roleName | Out-Null
                    Write-Verbose "$roleName role assigned successfully"
                }
            }
        }

        # Note about Key Vault RBAC mode
        $kvRoles = $permissions.AzureRoles | Where-Object { $_ -like 'Key Vault*' }
        if ($kvRoles.Count -gt 0) {
            Write-Verbose "Key Vault RBAC roles assigned. Note: This only works for vaults using Azure RBAC authorization mode."
        }
    }
    elseif ($SkipRoleAssignment -and $permissions.AzureRoles.Count -gt 0) {
        Write-Warning @"
Role assignment was skipped. You must assign these roles manually:
$($permissions.AzureRoles | ForEach-Object { " New-AzRoleAssignment -ObjectId '$principalId' -Scope '<scope>' -RoleDefinitionName '$_'" } | Out-String)
"@

    }
    #endregion

    #region Build Output
    $output = [PSCustomObject]@{
        WebAppName        = $WebAppName
        ResourceGroupName = $ResourceGroupName
        PrincipalId       = $principalId
        TenantId          = $tenantId
        Scope             = $Scope
        Permissions       = [PSCustomObject]@{
            Graph             = $permissions.Graph
            ARM               = $permissions.ARM
            KeyVaultDataPlane = $permissions.KeyVaultDataPlane
            AzureRoles        = $permissions.AzureRoles
            CheckCount        = $permissions.CheckCount
        }
        NextSteps         = @"
The managed identity for '$WebAppName' has been configured with CIEM permissions.

To use this managed identity in CIEM:
1. In your PSU instance, go to Configuration page
2. Select 'Managed Identity' as the authentication method
3. Save the configuration

The managed identity will automatically authenticate when running within Azure App Service.
No client secrets or certificates are needed.
"@

    }

    Write-Host "`nManaged identity configured successfully!" -ForegroundColor Green
    Write-Host "Web App: $WebAppName"
    Write-Host "Principal ID: $principalId"
    Write-Host "Tenant ID: $tenantId"
    Write-Host "Scope: $Scope"

    Write-Host "`nPermissions assigned:" -ForegroundColor Cyan
    Write-Host " Graph API permissions: $($permissions.Graph.Count)"
    if ($permissions.AzureRoles.Count -gt 0) {
        Write-Host " Azure RBAC roles: $(if (-not $SkipRoleAssignment) { $permissions.AzureRoles -join ', ' } else { 'Skipped' })"
    }

    Write-Host "`nTo use managed identity authentication in CIEM:" -ForegroundColor Yellow
    Write-Host " 1. Navigate to the PSU CIEM app Configuration page"
    Write-Host " 2. Select 'Managed Identity' as the authentication method"
    Write-Host " 3. Save the configuration"

    $output
    #endregion
}