Public/Grant-AvdUserToSessionHost.ps1

function Grant-AvdUserToSessionHost {
    <#
    .SYNOPSIS
    Assigns users to the first available session hosts in an AVD hostpool.
    .DESCRIPTION
    This function intelligently assigns users to available session hosts (hosts with no assigned user) in an Azure Virtual Desktop hostpool.
    It can handle single users or lists of users and will assign them to the first available session hosts.
    .PARAMETER HostpoolName
    Enter the AVD Hostpool name
    .PARAMETER ResourceGroupName
    Enter the AVD Hostpool resourcegroup name
    .PARAMETER SubscriptionId
    Enter the Azure subscription ID
    .PARAMETER UserPrincipalName
    Enter a single user principal name to assign (e.g., user@domain.com)
    .PARAMETER UserList
    Enter an array of user principal names to assign (e.g., @('user1@domain.com', 'user2@domain.com'))
    .PARAMETER Force
    Force assignment even if session host has active sessions
    .PARAMETER MaxAssignments
    Maximum number of assignments to make (default: unlimited)
    .EXAMPLE
    Grant-AvdUserToSessionHost -HostpoolName "hp-avd-personal" -ResourceGroupName "rg-avd-01" -SubscriptionId "ade317a3-a92e-4615-a8d0-30ae80dfa9a7" -UserPrincipalName "user@domain.com"
    .EXAMPLE
    Grant-AvdUserToSessionHost -HostpoolName "hp-avd-personal" -ResourceGroupName "rg-avd-01" -SubscriptionId "ade317a3-a92e-4615-a8d0-30ae80dfa9a7" -UserList @('user1@domain.com', 'user2@domain.com', 'user3@domain.com')
    .EXAMPLE
    Grant-AvdUserToSessionHost -HostpoolName "hp-avd-personal" -ResourceGroupName "rg-avd-01" -SubscriptionId "ade317a3-a92e-4615-a8d0-30ae80dfa9a7" -UserList @('user1@domain.com', 'user2@domain.com', 'user3@domain.com') -MaxAssignments 2
    #>

    [CmdletBinding(DefaultParameterSetName = 'SingleUser')]
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$HostpoolName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ResourceGroupName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SubscriptionId,

        [parameter(Mandatory, ParameterSetName = 'SingleUser')]
        [ValidateNotNullOrEmpty()]
        [string]$UserPrincipalName,

        [parameter(Mandatory, ParameterSetName = 'MultipleUsers')]
        [ValidateNotNullOrEmpty()]
        [string[]]$UserList,

        [parameter()]
        [switch]$Force,

        [parameter()]
        [switch]$WhatIf
    )

    Begin {
        Write-Verbose "Starting user assignment process for hostpool: $HostpoolName"
        AuthenticationCheck
        $token = GetAuthToken -resource $global:AzureApiUrl
        $batchApiUrl = "https://management.azure.com/batch?api-version=2020-06-01"
        
        # Validate that the hostpool is of type "Personal"
        Write-Verbose "Validating hostpool type for $HostpoolName"
        try {
            $hostpool = Get-AvdHostPool -HostpoolName $HostpoolName -ResourceGroupName $ResourceGroupName
            
            if ($hostpool.properties.hostPoolType -ne "Personal") {
                throw "User assignments can only be made to Personal hostpools. The hostpool '$HostpoolName' is of type '$($hostpool.properties.hostPoolType)'. Please use a Personal hostpool for user assignments."
            }
            
            Write-Verbose "Hostpool '$HostpoolName' is confirmed to be of type 'Personal' - proceeding with user assignments"
        }
        catch {
            if ($_.Exception.Message -like "*User assignments can only be made to Personal hostpools*") {
                throw $_
            }
            else {
                throw "Failed to retrieve hostpool information for '$HostpoolName' in resource group '$ResourceGroupName'. Please verify the hostpool exists and you have appropriate permissions. Error: $_"
            }
        }
        
        # Initialize results tracking
        $assignmentResults = @()
        $assignmentCount = 0
        $unassignedUsers = @()
        
    }

    Process {
        try {
            # Determine the list of users to assign
            $usersToAssign = switch ($PsCmdlet.ParameterSetName) {
                'SingleUser' { @($UserPrincipalName) }
                'MultipleUsers' { $UserList }
            }

            Write-Information "Processing $($usersToAssign.Count) user(s) for assignment" -InformationAction Continue

            # Get available session hosts using the existing Get-AvdUserAssignments function
            Write-Verbose "Retrieving session host information from hostpool..."
            $sessionHosts = Get-AvdUserAssignments -HostpoolName $HostpoolName -ResourceGroupName $ResourceGroupName -SubscriptionId $SubscriptionId -StatusFilter @("Available", "Disconnected")

            if (-not $sessionHosts -or $sessionHosts.Count -eq 0) {
                Write-Warning "No available session hosts found in hostpool $HostpoolName"
                return
            }

            # Filter for truly available hosts (no assigned user)
            $availableHosts = $sessionHosts | Where-Object { 
                ($null -eq $_.assignedUser) -or [string]::IsNullOrEmpty($_.assignedUser) -or $_.assignedUser -eq ""
            } | Sort-Object name

            Write-Information "Found $($availableHosts.Count) available session host(s) without assigned users" -InformationAction Continue

            if ($availableHosts.Count -eq 0) {
                Write-Warning "No session hosts available for assignment (all hosts already have assigned users)"
                return
            }

            # Calculate how many users need assignments (excluding already assigned users)
            $usersAlreadyAssigned = @($usersToAssign | Where-Object { 
                $user = $_
                $sessionHosts | Where-Object { $_.assignedUser -eq $user }
            }).Count
            
            $usersNeedingAssignment = $usersToAssign.Count - $usersAlreadyAssigned
            
            # Check if we have enough available session hosts
            if ($usersNeedingAssignment -gt $availableHosts.Count) {
                $additionalHostsNeeded = $usersNeedingAssignment - $availableHosts.Count
                Write-Warning "Insufficient session hosts available! You have $($availableHosts.Count) available session hosts but need to assign $usersNeedingAssignment users (excluding $usersAlreadyAssigned already assigned). You need $additionalHostsNeeded additional session host(s) to assign all users."
                Write-Information "Proceeding to assign users to the $($availableHosts.Count) available session hosts..." -InformationAction Continue
            }
            elseif ($usersNeedingAssignment -eq 0) {
                Write-Information "All $($usersToAssign.Count) users are already assigned to session hosts in this hostpool." -InformationAction Continue
            }
            else {
                Write-Information "You have sufficient session hosts ($($availableHosts.Count) available) to assign all $usersNeedingAssignment users needing assignment." -InformationAction Continue
            }

            # Process each user assignment
            foreach ($user in $usersToAssign) {
                # Check if user already has a session host assigned in this hostpool
                $existingAssignment = $sessionHosts | Where-Object { $_.assignedUser -eq $user }
                if ($existingAssignment) {
                    $existingSessionHostName = $existingAssignment.sessionHostName[0]
                    Write-Warning "User '$user' is already assigned to session host '$existingSessionHostName' in hostpool '$HostpoolName'. Skipping assignment."
                    
                    $assignmentResults += [PSCustomObject]@{
                        User = $user
                        SessionHost = $existingSessionHostName
                        Status = "Already Assigned - Skipped"
                        ResourceId = $existingAssignment.vmId
                        Message = "User already assigned to this session host"
                    }
                    continue
                }

                # Check if we have available hosts left
                if ($assignmentCount -ge $availableHosts.Count) {
                    Write-Warning "No more available session hosts for user: $user"
                    $unassignedUsers += $user
                    
                    $assignmentResults += [PSCustomObject]@{
                        User = $user
                        SessionHost = "N/A"
                        Status = "Not Assigned - No Available Hosts"
                        ResourceId = "N/A"
                        Message = "No available session hosts remaining"
                    }
                    continue
                }

                $targetHost = $availableHosts[$assignmentCount]
                $sessionHostName = $targetHost.sessionHostName[0]

                Write-Information "Assigning user '$user' to session host '$sessionHostName'" -InformationAction Continue

                if ($WhatIf) {
                    Write-Information "[WHATIF] Would assign user '$user' to session host '$sessionHostName'" -InformationAction Continue
                    $assignmentResults += [PSCustomObject]@{
                        User = $user
                        SessionHost = $sessionHostName
                        Status = "WhatIf - Would Assign"
                        ResourceId = $targetHost.vmId
                    }
                }
                else {
                    try {
                        # Construct the batch assignment request using the correct format
                        $forceString = if ($Force) { "true" } else { "false" }
                        $apiVersionLatest = "2025-03-01-preview"
                        
                        # Build the session host resource ID correctly
                        $sessionHostResourceId = $targetHost.id
                        $sessionHostUrl = $sessionHostResourceId -replace "^https://management\.azure\.com", ""
                        
                        $batchRequest = @{
                            requests = @(
                                @{
                                    content = @{
                                        id = $sessionHostResourceId
                                        name = $targetHost.name
                                        type = $targetHost.type
                                        properties = @{
                                            allowNewSession = $true
                                            assignedUser = $user
                                        }
                                    }
                                    httpMethod = "PATCH"
                                    name = [System.Guid]::NewGuid().ToString()
                                    requestHeaderDetails = @{
                                        commandName = "Microsoft_Azure_WVD.HostpoolVirtualMachineBladeV3.UpdateSessionHostsUsers"
                                    }
                                    url = "$sessionHostUrl" + "?api-version=$apiVersionLatest&force=$forceString"
                                }
                            )
                        }
                        
                        $batchUrl = "https://management.azure.com/batch?api-version=2020-06-01"
                        
                        $assignParameters = @{
                            uri = $batchUrl
                            Method = "POST"
                            Headers = $token
                            Body = ($batchRequest | ConvertTo-Json -Depth 10)
                        }

                        Write-Verbose "Making batch assignment API call for user: $user to session host: $sessionHostResourceId"
                        $batchResponse = Request-Api @assignParameters
                        
                        # Process the batch response
                        if ($batchResponse.responses -and $batchResponse.responses.Count -gt 0) {
                            $assignmentResponse = $batchResponse.responses[0]
                            
                            if ($assignmentResponse.httpStatusCode -eq 200) {
                                $response = $assignmentResponse.content
                            }
                            else {
                                throw "Assignment failed with status code: $($assignmentResponse.httpStatusCode). Error: $($assignmentResponse.content.error.message)"
                            }
                        }
                        else {
                            throw "No response received from batch assignment request"
                        }

                        Write-Information "Successfully assigned user '$user' to session host '$sessionHostName'" -InformationAction Continue
                        
                        $assignmentResults += [PSCustomObject]@{
                            User = $user
                            SessionHost = $sessionHostName
                            Status = "Successfully Assigned"
                            ResourceId = $targetHost.vmId
                            Response = $response
                        }
                    }
                    catch {
                        Write-Error "Failed to assign user '$user' to session host '$sessionHostName': $_"
                        
                        $assignmentResults += [PSCustomObject]@{
                            User = $user
                            SessionHost = $sessionHostName
                            Status = "Assignment Failed"
                            ResourceId = $targetHost.vmId
                            Error = $_.Exception.Message
                        }
                    }
                }

                $assignmentCount++
            }
            # Return results
            return $assignmentResults
        }
        catch {
            Write-Error "Error in Grant-AvdUserToSessionHost: $_"
            throw
        }
    }

    End {
         # Display summary
            Write-Information "`nAssignment Summary:" -InformationAction Continue
            Write-Information "Total users processed: $($usersToAssign.Count)" -InformationAction Continue
            Write-Information "Successful assignments: $(($assignmentResults | Where-Object { $_.Status -eq 'Successfully Assigned' -or $_.Status -eq 'WhatIf - Would Assign' }).Count)" -InformationAction Continue
            Write-Information "Failed assignments: $(($assignmentResults | Where-Object { $_.Status -eq 'Assignment Failed' }).Count)" -InformationAction Continue
            Write-Information "Already assigned (skipped): $(($assignmentResults | Where-Object { $_.Status -eq 'Already Assigned - Skipped' }).Count)" -InformationAction Continue
            Write-Information "Not assigned (no hosts available): $(($assignmentResults | Where-Object { $_.Status -eq 'Not Assigned - No Available Hosts' }).Count)" -InformationAction Continue
            
            # Display unassigned users if any
            if ($unassignedUsers.Count -gt 0) {
                Write-Warning "`nUsers not assigned due to insufficient session hosts ($($unassignedUsers.Count)):"
                $unassignedUsers | ForEach-Object { Write-Warning " - $_" }
            }
        Write-Verbose "User assignment process completed"
    }
}