Public/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 -CallQueue "My CQ"
 
    Associates Account1@domain.com to the Call Queue "My CQ"
  .EXAMPLE
    New-TeamsResourceAccountAssociation -UserPrincipalName Account1@domain.com -AutoAttendant "My AA" -Force
 
    Associates Account1@domain.com to the Auto Attendant "My AA"
    This will un-associate the Resource Account from any previous connection and associate it with this Auto Attendant
  .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')]
    [Alias('Identity', 'ObjectId')]
    [ValidateScript( {
        If ($_ -match '@' -or $_ -match $script:TFMatchGuid) { $True } else {
          throw [System.Management.Automation.ValidationMetadataException] 'Value must be a valid UPN or ObjectId'
        } })]
    [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)"

    # Asserting AzureAD Connection
    if ( -not $script:TFPSSA) { $script:TFPSSA = Assert-AzureADConnection; if ( -not $script:TFPSSA ) { 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' }

    #Initialising Counters
    $private:StepsID0, $private:StepsID1 = Get-WriteBetterProgressSteps -Code $($MyInvocation.MyCommand.Definition) -MaxId 1
    $private:ActivityID0 = $($MyInvocation.MyCommand.Name)
    [int] $private:CountID0 = [int] $private:CountID1 = 1

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

    #region Determining and Validating Entity
    $StatusID0 = 'Verifying input'
    # Determining $EntityObject
    $CurrentOperationID0 = 'Determining Entity Object'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    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
    $CurrentOperationID0 = 'Determining Entity Object is unique'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    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.Generic.List[object]]$Accounts = @()
    foreach ($UPN in $UserPrincipalName) {
      # re-Initialising counters for Progress bars (for Pipeline processing)
      [int] $private:CountID0 = 2
      $StatusID0 = 'Verifying input'
      $CurrentOperationID0 = 'Processing provided UserPrincipalNames'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      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
    }

    $StatusID0 = 'Processing found Resource Accounts'
    $CurrentOperationID0 = ''
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    [int] $private:StepsID1 = $StepsID1 * $(if ($Accounts.IsArray) { $Accounts.Count } else { 1 })
    [System.Collections.Generic.List[object]]$ValidatedAccounts = @()
    foreach ($Account in $Accounts) {
      $ActivityID1 = "'$($Account.UserPrincipalName)'"
      $StatusID1 = ''
      $CurrentOperationID1 = 'Querying existing associations'
      Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
      $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
      $CurrentOperationID1 = 'Validating ApplicationType'
      Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
      $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
          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
          }

          $CurrentOperationID1 = "Application Type is not '$DesiredType' - Waiting for AzureAD (2s)"
          Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
          Start-Sleep -Seconds 2

          $CurrentOperationID1 = "Application Type is not '$DesiredType' - Verifying"
          Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
          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 "SUCCESS: Resource Account '$($Account.UserPrincipalName)' - Changing Application Type to '$DesiredType'"
          }
        }
        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 -Activity $ActivityID1 -Completed
    }

    # Processing found accounts
    if ( $ValidatedAccounts ) {
      [int] $private:StepsID1 = $ValidatedAccounts.Count
      $StatusID0 = "Processing $private:StepsID1 validated Resource Accounts"
      $CurrentOperationID0 = ''
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      # Processing Assignment
      foreach ($Account in $ValidatedAccounts) {
        [int] $private:CountID1 = 1
        $OperationStatus = $ErrorEncountered = $null
        $ActivityID1 = "'$($Account.UserPrincipalName)'"
        $StatusID1 = "Assignment to $DesiredType '$($EntityObject.Name)'"
        $CurrentOperationID1 = "Associating '$($Account.UserPrincipalName)' with $DesiredType"
        Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1

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

        # Create CsAutoAttendantCallableEntity
        if ($PSBoundParameters.ContainsKey('Debug') -or $DebugPreference -eq 'Continue') {
          " Function: $($MyInvocation.MyCommand.Name) - Parameters (New-CsOnlineApplicationInstanceAssociation)", ($Parameters | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
        }

        if ($PSCmdlet.ShouldProcess("$($Account.UserPrincipalName)", 'New-CsOnlineApplicationInstanceAssociation')) {
          try {
            $OperationStatus = New-CsOnlineApplicationInstanceAssociation @Parameters
          }
          catch {
            Write-Verbose -Message "New-CsOnlineApplicationInstance failed with: $($_.Exception.Message)"
            $ErrorEncountered = $_
          }
        }

        # Re-query Association Target
        # Wating for AAD to write the Association Target so that it may be queried correctly
        Start-Sleep -Seconds 2
        if ( $OperationStatus ) {
          $CurrentOperationID1 = "Validating association of '$($Account.UserPrincipalName)' with $DesiredType"
          Write-BetterProgress -Id 1 -Activity $ActivityID1 -Status $StatusID1 -CurrentOperation $CurrentOperationID1 -Step ($private:CountID1++) -Of $private:StepsID1
          $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]@{
          PSTypeName        = 'PowerShell.TeamsFunctsions.ResourceAccount.Association'
          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 -Activity $ActivityID1 -Completed
        Write-Progress -Id 0 -Activity $ActivityID0 -Completed
        Write-Output $ResourceAccountAssociationObject

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

  } #process

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
  } #end
} #New-TeamsResourceAccountAssociation