Public/Functions/ResourceAccount/New-TeamsResourceAccountAssociation.ps1

# Module: TeamsFunctions
# Function: ResourceAccount
# Author: David Eberhardt
# Updated: 01-DEC-2020
# Status: Live




function New-TeamsResourceAccountAssociation {
  <#
  .SYNOPSIS
    Connects one or more Resource Accounts to a single CallQueue or AutoAttendant
  .DESCRIPTION
    Associates one or more existing Resource Accounts to a Call Queue or Auto Attendant
    Resource Account Type is checked against the ApplicationType.
    User is prompted if types do not match
  .PARAMETER UserPrincipalName
    Required. UPN(s) of the Resource Account(s) to be associated to a Call Queue or AutoAttendant
  .PARAMETER CallQueue
    Optional. Specifies the connection to be made to the provided Call Queue Name
  .PARAMETER AutoAttendant
    Optional. Specifies the connection to be made to the provided Auto Attendant Name
  .PARAMETER Force
    Optional. Suppresses Confirmation dialog if -Confirm is not provided
    Used to override prompts for alignment of ApplicationTypes.
    The Resource Account is changed to have the same type as the associated Object (CallQueue or AutoAttendant)!
  .EXAMPLE
    New-TeamsResourceAccountAssociation -UserPrincipalName Account1@domain.com -
    Explanation of what the example does
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    Connects multiple Resource Accounts to ONE CallQueue or AutoAttendant
    The Type of the Resource Account has to corellate to the entity connected.
    Parameter Force can be used to change the type of RA to align to the entity if possible.
  .COMPONENT
    TeamsResourceAccount
    TeamsAutoAttendant
    TeamsCallQueue
  .FUNCTIONALITY
    Creates a new Association between an unassociated Resource Account and an Auto Attendant or a Call Queue
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/New-TeamsResourceAccountAssociation.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/about_TeamsResourceAccount.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/master/docs/
  #>

  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'CallQueue')]
  [Alias('New-TeamsRAA')]
  [OutputType([System.Object])]
  param(
    [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'UPN of the Object to change')]
    [string[]]$UserPrincipalName,

    [Parameter(Mandatory, ParameterSetName = 'CallQueue', ValueFromPipelineByPropertyName, HelpMessage = 'Name of the CallQueue')]
    [string]$CallQueue,

    [Parameter(Mandatory, ParameterSetName = 'AutoAttendant', ValueFromPipelineByPropertyName, HelpMessage = 'Name of the AutoAttendant')]
    [string]$AutoAttendant,

    [Parameter(Mandatory = $false)]
    [switch]$Force
  ) #param

  begin {
    Show-FunctionStatus -Level Live
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"
    Write-Verbose -Message "Need help? Online: $global:TeamsFunctionsHelpURLBase$($MyInvocation.MyCommand)`.md"

    # Asserting AzureAD Connection
    if (-not (Assert-AzureADConnection)) { break }

    # Asserting MicrosoftTeams Connection
    if (-not (Assert-MicrosoftTeamsConnection)) { break }

    # Setting Preference Variables according to Upstream settings
    if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') }
    if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') }
    if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') }
    if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' }
    if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' }

    # Enabling $Confirm to work with $Force
    if ($Force -and -not $Confirm) {
      $ConfirmPreference = 'None'
    }

    # Initialising counters for Progress bars - Level 0
    [int]$step = 0
    [int]$sMax = 5

    #region Determining and Validating Entity
    # Determining $EntityObject
    $Status = 'Validation'
    $Operation = 'Determining Entity'
    Write-Progress -Id 0 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step / $sMax * 100)
    Write-Verbose -Message "$Status - $Operation"
    try {
      switch ($PSCmdlet.ParameterSetName) {
        'CallQueue' {
          $DesiredType = 'CallQueue'
          $Entity = $CallQueue
          # Querying Call Queue by Name - need Unique Result
          Write-Verbose -Message "Querying Call Queue '$CallQueue'"
          $EntitySearch = Get-CsCallQueue -NameFilter "$CallQueue" -WarningAction SilentlyContinue
        }
        'AutoAttendant' {
          $DesiredType = 'AutoAttendant'
          $Entity = $AutoAttendant
          # Querying Auto Attendant by Name - need Unique Result
          Write-Verbose -Message "Querying Auto Attendant '$AutoAttendant'"
          $EntitySearch = Get-CsAutoAttendant -NameFilter "$AutoAttendant" -WarningAction SilentlyContinue
        }
      }
    }
    catch {
      throw "Cannot determine $DesiredType '$Entity'"
    }
    if ($EntitySearch.Count -gt 1) {
      $EntityObject = $EntitySearch | Where-Object Name -EQ "$Entity"
    }
    else {
      $EntityObject = $EntitySearch
    }

    # Validating Unique result received
    $Operation = 'Unique result'
    $step++
    Write-Progress -Id 0 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step / $sMax * 100)
    Write-Verbose -Message "$Status - $Operation"
    if ($null -eq $EntityObject) {
      throw [System.Exception]::New("$DesiredType '$Entity' - Not found, please check entity exists with this Name" )
    }
    elseif ($EntityObject -is [Array]) {
      $EntityObject = $EntityObject | Where-Object Name -EQ "$Entity"
      Write-Verbose -Message "'$Entity' - Multiple results found! This script is based on lookup via Name, which requires, for safety reasons, a unique Name to process." -Verbose
      Write-Verbose -Message 'Listing all objects found with the Name. Please use the correct Identity to run New-CsOnlineApplicationInstanceAssociation!' -Verbose
      $EntityObject | Select-Object Identity, Name | Format-Table
      throw [System.Exception]::New("$DesiredType '$Entity' - Multiple Results found! Cannot determine unique result. Please provide GUID or use New-CsOnlineApplicationInstanceAssociation!" )
    }
    else {
      Write-Verbose -Message "$DesiredType '$Entity' - Unique result found: $($EntityObject.Name)"
    }
    #endregion

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    # Query $UserPrincipalName
    [System.Collections.ArrayList]$Accounts = @()
    $Operation = 'Processing provided UserPrincipalNames'
    $step++
    Write-Progress -Id 0 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step / $sMax * 100)
    Write-Verbose -Message "$Status - $Operation"
    foreach ($UPN in $UserPrincipalName) {
      Write-Verbose -Message "Querying Resource Account '$UPN'"
      try {
        $RAObject = Get-AzureADUser -ObjectId "$UPN" -WarningAction SilentlyContinue -ErrorAction Stop
        $AppInstance = Get-CsOnlineApplicationInstance $RAObject.ObjectId -WarningAction SilentlyContinue -ErrorAction Stop
        [void]$Accounts.Add($AppInstance)
        Write-Verbose "Resource Account found: '$($AppInstance.DisplayName)'"
      }
      catch {
        Write-Error "Resource Account not found: '$UPN'" -Category ObjectNotFound
        continue
      }
    }

    # Breaks the chain if no eligible accounts are found.
    if ( -not $Accounts ) {
      Write-Warning -Message 'No Resource Accounts found eligible for Association. Stopping.'
      return
    }

    $Operation = 'Processing found Resource Accounts'
    $step++
    Write-Progress -Id 0 -Status $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step / $sMax * 100)
    Write-Verbose -Message $Operation
    $Counter = 1
    [System.Collections.ArrayList]$ValidatedAccounts = @()
    foreach ($Account in $Accounts) {
      $Status = 'Processing'
      $Operation = "'$($Account.UserPrincipalName)'"
      Write-Progress -Id 0 -Status $Status -Activity $MyInvocation.MyCommand -PercentComplete ($Counter / $($Accounts.Count) * 100)
      Write-Verbose -Message "$Status - $Operation"
      $Counter++
      # Query existing connection

      # Initialising counters for Progress bars - Level 0
      [int]$step2 = 1
      [int]$sMax2 = 6

      $Status = "'$($Account.UserPrincipalName)'"
      $Operation = 'Querying existing associations'
      Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step2 / $sMax2 * 100)
      Write-Verbose -Message "$Status - $Operation"
      $ExistingConnection = $null
      $ExistingConnection = Get-CsOnlineApplicationInstanceAssociation -Identity $Account.ObjectId -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

      if ($null -eq $ExistingConnection.ConfigurationId) {
        Write-Verbose -Message "'$($Account.UserPrincipalName)' - No assignment found. OK"
      }
      else {
        Write-Error -Message "'$($Account.UserPrincipalName)' - This account cannot be associated as it is already assigned as '$($ExistingConnection.ConfigurationType)'"
        continue
      }

      # Comparing ApplicationType
      $Operation = 'Validating ApplicationType'
      $step2++
      Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step2 / $sMax2 * 100)
      Write-Verbose -Message "$Status - $Operation"
      $ApplicationTypeMatches = ((Get-CsOnlineApplicationInstance -Identity "$($Account.UserPrincipalName)" -WarningAction SilentlyContinue).ApplicationId -eq (GetAppIdFromApplicationType $DesiredType))

      if ( $ApplicationTypeMatches ) {
        Write-Verbose -Message "'$($Account.UserPrincipalName)' - Application type matches '$DesiredType' - OK"
      }
      else {
        if ($PSBoundParameters.ContainsKey('Force')) {
          # Changing Application Type
          $Operation = "Application Type is not '$DesiredType' - Changing"
          $step2++
          Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step2 / $sMax2 * 100)
          Write-Verbose -Message "$Status - $Operation"
          try {
            $null = Set-CsOnlineApplicationInstance -Identity $Account.ObjectId -ApplicationId $(GetAppIdFromApplicationType $DesiredType) -ErrorAction Stop
          }
          catch {
            Write-Error -Message "'$($Account.UserPrincipalName)' - Application type does not match and could not be changed! Expected: '$DesiredType' - Please change manually or recreate the Account" -Category InvalidType -RecommendedAction 'Please change manually or recreate the Account'
            continue
          }

          $Operation = "Application Type is not '$DesiredType' - Waiting for AzureAD (2s)"
          $step2++
          Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step2 / $sMax2 * 100)
          Write-Verbose -Message "$Status - $Operation"
          Start-Sleep -Seconds 2

          $Operation = "Application Type is not '$DesiredType' - Verifying"
          $step2++
          Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step2 / $sMax2 * 100)
          Write-Verbose -Message "$Status - $Operation"
          if ($DesiredType -ne $(GetApplicationTypeFromAppId (Get-CsOnlineApplicationInstance -Identity "($($Account.ObjectId)" -WarningAction SilentlyContinue).ApplicationId)) {
            Write-Error -Message "'$($Account.UserPrincipalName)' - Application type could not be changed to Desired Type: '$DesiredType'" -Category InvalidType
            continue
          }
          else {
            Write-Information "'$($Account.UserPrincipalName)' - Changing Application Type to '$DesiredType': SUCCESS"
          }
        }
        else {
          Write-Warning -Message "'$($Account.UserPrincipalName)' - Application type does not match! Expected '$DesiredType' - Omitting account. Please change type manually or use -Force switch"
          continue
        }
      }

      [void]$ValidatedAccounts.Add($Account)
      Write-Progress -Id 1 -Status 'Complete' -Activity $MyInvocation.MyCommand -Completed
    }

    # Processing found accounts
    if ( $ValidatedAccounts ) {
      # Processing Assignment
      Write-Verbose -Message "Processing assignment of all Accounts to $DesiredType '$($EntityObject.Name)'"
      $Counter = 1
      foreach ($Account in $Accounts) {
        $ErrorEncountered = $null

        $Status = 'Assignment'
        $Operation = "'$($Account.UserPrincipalName)'"
        Write-Progress -Id 0 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($Counter / $($Accounts.Count) * 100)
        Write-Verbose -Message "$Status - $Operation"
        $Counter++

        # Initialising counters for Progress bars - Level 0
        [int]$step3 = 1
        [int]$sMax3 = 4

        # Establishing Association
        $Status = "'$($Account.UserPrincipalName)'"
        $Operation = "Assigning to $DesiredType '$($EntityObject.Name)'"
        Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step3 / $sMax3 * 100)
        Write-Verbose -Message "$Status - $Operation"

        # Creating Splatting Object
        $Parameters = $null
        $Parameters += @{ 'Identities' = $Account.ObjectId }
        $Parameters += @{ 'ConfigurationType' = $DesiredType }
        $Parameters += @{ 'ConfigurationId' = $EntityObject.Identity }
        $Parameters += @{ 'ErrorAction' = 'Stop' }

        # Create CsAutoAttendantCallableEntity
        Write-Verbose -Message '[PROCESS] Creating Resource Account Association'
        if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
          "Function: $($MyInvocation.MyCommand.Name): Parameters:", ($Parameters | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }

        if ($PSCmdlet.ShouldProcess("$($Account.UserPrincipalName)", 'New-CsOnlineApplicationInstanceAssociation')) {
          #$OperationStatus = New-CsOnlineApplicationInstanceAssociation -Identities $Account.ObjectId -ConfigurationType $DesiredType -ConfigurationId $EntityObject.Identity
          try {
            $OperationStatus = New-CsOnlineApplicationInstanceAssociation @Parameters
          }
          catch {
            $ErrorEncountered = $_
          }
        }

        # Re-query Association Target
        # Wating for AAD to write the Association Target so that it may be queried correctly
        $Operation = "Assigning to $DesiredType '$($EntityObject.Name)' - Waiting for AzureAD (2s)"
        $step3++
        Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step3 / $sMax3 * 100)
        Write-Verbose -Message "$Status - $Operation"
        Start-Sleep -Seconds 2

        $Operation = "Assigning to $DesiredType '$($EntityObject.Name)' - Verifying"
        $step3++
        Write-Progress -Id 1 -Status $Status -CurrentOperation $Operation -Activity $MyInvocation.MyCommand -PercentComplete ($step3 / $sMax3 * 100)
        Write-Verbose -Message "$Status - $Operation"
        $AssociationTarget = switch ($PSCmdlet.ParameterSetName) {
          'CallQueue' {
            Get-CsCallQueue -Identity $OperationStatus.Results.ConfigurationId -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
          }
          'AutoAttendant' {
            Get-CsAutoAttendant -Identity $OperationStatus.Results.ConfigurationId -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
          }
        }

        # Output
        $ResourceAccountAssociationObject = $null
        $ResourceAccountAssociationObject = [PSCustomObject][ordered]@{
          UserPrincipalName = $Account.UserPrincipalName
          ConfigurationType = $OperationStatus.Results.ConfigurationType
          Result            = $OperationStatus.Results.Result
          StatusCode        = $OperationStatus.Results.StatusCode
          StatusMessage     = $OperationStatus.Results.Message
          AssociatedTo      = $AssociationTarget.Name
        }

        Write-Progress -Id 1 -Status "'$($Account.UserPrincipalName)' - Complete" -Activity $MyInvocation.MyCommand -Completed
        Write-Output $ResourceAccountAssociationObject

        if ( $ErrorEncountered ) {
          Write-Error -Message "Association of Object failed with exception: $($ErrorEncountered.Exception.Message)" -ErrorAction Stop
        }
      }
    }

  } #process

  end {
    Write-Progress -Id 0 -Status 'Complete' -Activity $MyInvocation.MyCommand -Completed
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
  } #end
} #New-TeamsResourceAccountAssociation