Public/AdminRole/Enable-GraphAdminRole.ps1

# Module: Orbit
# Function: UserAdmin
# Author: David Eberhardt
# Updated: 27-MAY 2023
# Status: Beta




function Enable-GraphAdminRole {
  <#
  .SYNOPSIS
    Enables eligible Admin Roles
  .DESCRIPTION
    Azure Ad Privileged Identity Management can require you to activate Admin Roles.
    Eligibe roles or groups can be activated with this Command
  .PARAMETER Identity
    Username of the Admin Account to enable roles for
  .PARAMETER Reason
    Optional. Small statement why these roles are requested
    By default, "Administration" is used as the reason.
  .PARAMETER Duration
    Optional. Integer. By default, enables Roles for 4 hours.
    Depending on your Administrators settings, values between 1 and 24 hours can be specified
  .PARAMETER TicketNumber
    Optional. Integer. Only used if provided
    Depending on your Administrators settings, a ticket number may be required to process the request
  .PARAMETER Extend
    Optional. Switch. If an assignment is already active, it can be extended.
    This will leave an open request which can be closed manually.
  .PARAMETER ProviderId
    Optional. Default is 'aadRoles' for the ProviderId, however, this script could also be used for activating
    Azure Resources ('azureResources'). Use with Confirm and EnableAll.
  .PARAMETER PassThru
    Optional. Displays output object for each activated Role
    Used for further processing to verify command was successful
  .PARAMETER Force
    Optional. Overrides confirmation dialog and enables all eligible roles
  .EXAMPLE
    Enable-GraphAdminRole John@domain.com
 
    Enables all eligible Teams Admin roles for User John@domain.com
  .EXAMPLE
    Enable-GraphAdminRole John@domain.com -EnableAll -Reason "Need to provision Users" -Duration 4
 
    Enables all eligible Admin roles for User John@domain.com with the reason provided.
  .EXAMPLE
    Enable-GraphAdminRole John@domain.com -EnableAll -ProviderId azureResources -Confirm
 
    Enables all eligible Azure Resources for User John@domain.com with confirmation for each Resource.
  .EXAMPLE
    Enable-GraphAdminRole John@domain.com -Extend -Duration 3
 
    If already activated, will extend the Azure Resources for User John@domain.com for up to 3 hours.
  .INPUTS
    System.String
  .OUTPUTS
    System.Void - Default Behaviour
    System.Object - With Switch PassThru
    Boolean - If called by other CmdLets
  .NOTES
    Limitations: MFA must be authorised first - Current workaround triggers MFA auth upon login.
    If the activation fails, please sign into Office.com or use https://aka.ms/myroles
    Once Authorised, this command can be used to activate your eligible Admin Roles.
    AzureResources provider activation is not yet tested.
    Thanks to Nathan O'Bryan, MVP|MCSM - nathan@mcsmlab.com for inspiring this script through Activate-PIMRole.ps1
  .COMPONENT
    UserManagement
  .FUNCTIONALITY
    Enables eligible Privileged Identity roles for Administration of Teams
  .LINK
    https://github.com/DEberhardt/Orbit/tree/main/docs/Orbit.Authentication/Enable-GraphAdminRole.md
  .LINK
    https://github.com/DEberhardt/Orbit/tree/main/docs/about/about_UserManagement.md
  .LINK
    https://github.com/DEberhardt/Orbit/tree/main/docs/
  #>


  [CmdletBinding(SupportsShouldProcess)]
  [OutputType([Void])]

  param(
    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Enter the identity of the Admin Account')]
    [Alias('UserPrincipalName', 'Id', 'ObjectId')]
    [string]$Identity,

    [Parameter(HelpMessage = 'Optional Reason for the request')]
    [string]$Reason,

    [Parameter(HelpMessage = 'Integer in hours to activate role(s) for')]
    [int]$Duration,

    [Parameter(HelpMessage = 'Ticket Number for use to provide to the request')]
    [int]$TicketNumber,

    [Parameter(HelpMessage = 'Azure ProviderId to be used')]
    [ValidateSet('aadRoles', 'azureResources')]
    [string]$ProviderId = 'aadRoles',

    [Parameter(HelpMessage = 'Tries to extend the activation.')]
    [switch]$Extend,

    [Parameter(HelpMessage = 'Displays output of activated roles to verify')]
    [switch]$PassThru,

    [Parameter(HelpMessage = 'Overrides confirmation dialog and enables all eligible roles')]
    [switch]$Force

  ) #param

  begin {
    Show-OrbitFunctionStatus -Level Beta
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"

    # Asserting Graph Connection
    if ( -not (Test-GraphConnection) ) { throw 'Connection to Microsoft Graph not established. Please validate connection' }

    $Stack = Get-PSCallStack
    $Called = ($stack.length -ge 3)

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

    #Loading Modules
    $CurrentOperationID0 = 'Loading modules'
    Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
    $GraphGovernanceModule = Get-InstalledModule Microsoft.Graph.Identity.Governance
    if ( -not $GraphGovernanceModule ) { Import-Module Microsoft.Graph.Identity.Governance -Force -Global -Verbose:$false }


    # Concept
    <#
    if no TenantId, Connect to Teams, record the TenantId, if Get-CsTenant returns something, OK, otherwise (if 'access denied'/403) then disconnect
    once TenantId is known Connect to Graph with that TenantId, Enable Admin Roles, wait/verify, Connect to Teams
    #>


    # preparing Splatting Object
    $Parameters = @{}
    $Parameters.ErrorAction = 'Stop'

    #region Supporting Parameters
    # Duration
    if ( -not $Duration ) {
      #TEST Duration is still the same
      [int]$Duration = 4
      # Duration is used in $Schedule
    }

    # Reason & Ticket Number
    if ( -not $Reason ) { $Reason = 'Administration' }
    if ( $TicketNr ) {
      #TEST TicketNr is avaialable with Graph
      $Parameters.$TicketNumber = $TicketNumber
      $Reason = "Ticket: $TicketNumber - $Reason"
      #$TicketSystem - how to add?
    }
    $Parameters.Reason = $Reason

    # ProviderId is hardcoded (or overridden by providing a value)
    #ProviderID is not a parameter on New-MgPrivilegedRoleAssignmentRequest
    #Write-Verbose -Message "Using Azure Provider Id: $ProviderId"
    #$Parameters.ProviderId = $ProviderId

    # ResourceId - is the Tenant Id
    Write-Verbose -Message 'Querying Azure Tenant Id'
    #ResourceId is not a parameter on New-MgPrivilegedRoleAssignmentRequest
    #TEST Id is available, but no guidance on what to plug into it.
    #$ResourceId = (Get-MgContext).TenantId
    #$Parameters.ResourceId = $ResourceId

    # Assignment state is always Active
    #TEST AssignmentState - Still necessary?
    $Parameters.AssignmentState = 'Active'

    # Connecting Graph Scopes
    Write-Information "INFO: Establishing Graph Connection with RoleManagement Scope (RoleManagement.Read.Directory,Directory.Read.All)"
    Connect-MgGraph -Scopes RoleManagement.Read.Directory,Directory.Read.All

    # Setting Graph Profile to Beta to enable PIM Commands
    Select-MgProfile -Name beta

    #TEST MOVE TO PROCESS for single Identity?
    #region Importing all Roles
    Write-Verbose -Message 'Querying Azure Privileged Role Definitions'
    try {
      $MyEligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty * -ErrorAction Stop
      $MyActiveRoles = Get-MgRoleManagementDirectoryRoleAssignmentSchedule -All -ExpandProperty * -ErrorAction Stop

      $PIMRoles = $MyEligibleRoles + $MyActiveRoles

      #TEST Report needed? How does the Raw data look??
      $Report = [System.Collections.Generic.List[Object]]::new()

      foreach ($a in $PIMRoles) {
          $regex = "^([^.]+)\.([^.]+)\.(.+)$"
          $a.Principal.AdditionalProperties.'@odata.type' -match $regex | out-null

          $obj = [PSCustomObject][Ordered]@{
              Assigned                 = $a.Principal.AdditionalProperties.displayName
              "Assigned Type"          = $matches[3]
              "Assigned Role"          = $a.RoleDefinition.DisplayName
              "Assigned Role Scope"    = $a.directoryScopeId
              "Assignment Type"        = (&{if ($a.AssignmentType -eq "Assigned") {"Active"} else {"Eligible"}})
              "Is Built In"            = $a.roleDefinition.isBuiltIn
              "Created Date"           = $a.CreatedDateTime
              "Expiration type"        = $a.ScheduleInfo.Expiration.type
              "Expiration Date"        = switch ($a.ScheduleInfo.Expiration.EndDateTime) {
                  {$a.ScheduleInfo.Expiration.EndDateTime -match '20'} {$a.ScheduleInfo.Expiration.EndDateTime}
                  {$a.ScheduleInfo.Expiration.EndDateTime -notmatch '20'} {"N/A"}
              }
          }
          $report.Add($obj)
      }

      $Report | Write-Debug
    }
    catch {
      #TEST Unknown whether these commands are actually throw this error
      #TEST Error thrown if Scopes are not right. - maybe catch that?
      if ($_.Exception.Message.Contains('The tenant needs an AAD Premium 2 license')) {
        Write-Error -Message 'Cannot query role definitions. Entra ID Premium License Required' -ErrorAction Stop
      }
      else {
        Write-Error -Message "Cannot query role definitions. Exception: $($_.Exception.Message)" -ErrorAction Stop
      }
    }
    #endregion

    #CHECK is this really needed anymore? Duration is an INT (maybe)?
    #region Defining Schedule
    Write-Verbose -Message "Creating Schedule based on Duration: $Duration hours"
    $Date = Get-Date
    $start = $Date.ToUniversalTime()
    $end = $Date.AddHours($Duration).ToUniversalTime()

    #TEST this should work without having to add the type
    $schedule = New-Object Microsoft.Open.MSGraph.Model.AzureADMSPrivilegedSchedule
    $schedule.Type = 'Once'
    $schedule.StartDateTime = $start.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
    $schedule.endDateTime = $end.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
    Write-Verbose -Message "Admin Roles will be active for $Duration hours, until: $($end.ToString())"
    $Parameters.Schedule = $schedule
    #endregion
    #endregion

    # Identity is not mandatory, using connected Session
    if ( -not $PSBoundParameters['Identity'] ) {
      $Identity = (Get-MgContext).Account
      Write-Information "INFO: No Identity Provided, using user currently connected to Graph: '$Identity'"
    }

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"

    #TEST If using 'self' cmdlets, foreach may not be needed
    foreach ($Id in $Identity) {
      #region Querying User Account
      Write-Verbose -Message "Processing User '$Id'"
      try {
        $SubjectId = (Get-MgUser -UserId "$Id" -WarningAction SilentlyContinue -ErrorAction STOP).Id
      }
      catch {
        Write-Error -Message 'User Account not valid' -Category ObjectNotFound -RecommendedAction 'Verify Identity/UserPrincipalName'
        continue
      }
      #endregion

      #region Processing Roles
      #TEST Don't know if TenantRoles are still needed, All roles already determined in $PIMRoles
      <#
      # Determining Direct assignments
      #Write-Verbose -Message "User '$Id' Querying all AzureADMSPrivilegedRoleAssignment"
      #$TenantRoles = Get-AzureADMSPrivilegedRoleAssignment -ProviderId $ProviderId -ResourceId $ResourceId
 
      # Determining Direct assignments
      Write-Verbose -Message "User '$Id' Determining direct assignments"
      #$MyRoles = Get-AzureADMSPrivilegedRoleAssignment -ProviderId $ProviderId -ResourceId $ResourceId -Filter "subjectId eq '$SubjectId'"
      $MyRoles = $TenantRoles | Where-Object SubjectId -EQ "$SubjectId"
      $MyActiveRoles = $MyRoles | Where-Object AssignmentState -EQ 'Active'
      $MyEligibleRoles = $MyRoles | Where-Object AssignmentState -EQ 'Eligible'
      Write-Verbose -Message "User '$Id' has currently $($MyActiveRoles.Count) of $($MyEligibleRoles.Count) activated"
      #>


      [System.Collections.Generic.List[object]]$Roles = @()
      #region Adding Direct assigned Roles
      if ($MyEligibleRoles.Count -gt 0) {
        #TODO Filter Teams Roles and adjacent? What if Global Admin is here? Do we really want to activate ALL roles assigned?
        foreach ($Role in $MyEligibleRoles) { [void]$Roles.Add($Role) }
      }

      if ( $MyEligibleRoles.Count -eq 0 ) {
        if ( $MyActiveRoles.Count -eq 0 ) {
          Write-Warning -Message "User '$Id' No eligible Privileged Access Roles availabe!"
          Continue
        }
        else {
          Write-Information "INFO: User '$Id' No eligible Privileged Access Roles availabe, but User has $($MyActiveRoles.Count) active Roles"
          return $(if ($Called) { $true })
        }
      }
      else {
        Write-Verbose "User '$Id' has direct assignments for $($MyEligibleRoles.Count) roles"
      }
      #endregion

      #region Activating Role
      [System.Collections.Generic.List[object]]$ActivatedRoles = @()
      foreach ($R in $Roles) {
        #TEST entire FOREACH need to be rewritten around
        <#
        $params = @{
          Reason = "reason-value"
          Duration = "duration-value"
          TicketNumber = "ticketNumber-value"
          TicketSystem = "ticketSystem-value"
        }
        Invoke-MgSelfPrivilegedRoleActivate -PrivilegedRoleId $privilegedRoleId -BodyParameter $params
 
        #CHECK whether to use MgSelf or New-MgPrivilegedRoleAssignmentRequest
        $params = @{
          Duration = "2"
          Reason = "Activate the role for business purpose"
          TicketNumber = "234"
          TicketSystem = "system"
          Schedule = @{
            StartDateTime = [System.DateTime]::Parse("2018-02-08T02:35:17.903Z")
          }
          Type = "UserAdd"
          AssignmentState = "Active"
          RoleId = "88d8e3e3-8f55-4a1e-953a-9b9898b8876b"
        }
        New-MgPrivilegedRoleAssignmentRequest -BodyParameter $params
        # https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.governance/new-mgprivilegedroleassignmentrequest?view=graph-powershell-beta
        # https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.governance/invoke-mgselfprivilegedroleassignmentrequestroleinfoactivate?view=graph-powershell-beta
        #>



        # Querying Role Display Name
        $RoleName = $AllRoles | Where-Object { $_.Id -eq $R.RoleDefinitionId } | Select-Object -ExpandProperty DisplayName

        # Confirm every role if not Force
        if ($PSCmdlet.ShouldProcess("$RoleName")) {
          if (-not ($Force -or $PSCmdlet.ShouldContinue("Eligible Role '$RoleName' found. Activate Role?", 'Enable-GraphAdminRole'))) {
            continue # user replied no
          }
          else {
            # Preparing Output object
            $ActivatedRole = @()
            $ActivatedRole = [PsCustomObject][ordered]@{
              PSTypeName    = 'PowerShell.TeamsFunctsions.AzureAdAdminRole.RoleActivation'
              'User'        = $Id
              'Rolename'    = $RoleName
              'Type'        = $null
              'ActiveUntil' = $null
            }

            # Adding Role Definition Id
            if ($Parameters.RoleDefinitionId) { $Parameters.RoleDefinitionId = $R.RoleDefinitionId }
            else { $Parameters.RoleDefinitionId = $R.RoleDefinitionId }

            # Determining Activation Type (UserAdd VS UserRenew)
            # NOTE The value for the Request type can be AdminAdd, UserAdd, AdminUpdate, AdminRemove, UserRemove, UserExtend, UserRenew, AdminRenew and AdminExtend.
            # NOTE More options could be provided than UserExtend (Request) and UserAdd. Bears investigation
            #>
            if ( $PSBoundParameters['Extend'] -and $R.RoleDefinitionId -in $MyActiveRoles.RoleDefinitionId ) {
              Write-Verbose -Message "User '$Id' Role '$RoleName' is already active and will be extended"
              $ActivatedRole.Type = 'UserExtend'
              if ($Parameters.Type) {
                $Parameters.Type = 'UserExtend'
              }
              else {
                $Parameters.Type = 'UserExtend'
              }
            }
            else {
              Write-Verbose -Message "User '$Id' Role '$RoleName' is currently not active and will be activated"
              $ActivatedRole.Type = 'UserAdd'
              if ($Parameters.Type) {
                $Parameters.Type = 'UserAdd'
              }
              else {
                $Parameters.Type = 'UserAdd'
              }
            }

            # Adding SubjectId to Parameters
            #if ($Parameters.SubjectId) { $Parameters.SubjectId = $SubjectId } else { $Parameters.SubjectId = $SubjectId }
            if ($Parameters.SubjectId) { $Parameters.SubjectId = $Role.SubjectId } else { $Parameters.SubjectId = $Role.SubjectId }

            #Activating the Role
            try {
              Write-Verbose -Message "User '$Id' Role '$RoleName': Activating Role"
              $ActivatedRole.ActiveUntil = $schedule.endDateTime
              if ($PSBoundParameters['Debug'] -or $DebugPreference -eq 'Continue') {
                "Function: $($MyInvocation.MyCommand.Name) - Parameters (Open-AzureADMSPrivilegedRoleAssignmentRequest)", ( $Parameters | Format-Table -AutoSize | Out-String).Trim() | Write-Debug
              }
              #$null = Open-AzureADMSPrivilegedRoleAssignmentRequest @Parameters
              $null = New-MgPrivilegedRoleAssignmentRequest -BodyParameter $Parameters
              [void]$ActivatedRoles.Add($ActivatedRole)
            }
            catch {
              if ($_.Exception.Message.Contains('UnauthorizedAccessException')) {
                Write-Error -Message 'Attempted to perform an unauthorized operation.' -Category InvalidData
              }
              elseif ($_.Exception.Message.Contains('EligibilityRule')) {
                Write-Error -Message 'User is not eligible to activate this role.' -Category InvalidData
              }
              elseif ($_.Exception.Message.Contains('The following policy rules failed: ["MfaRule"]')) {
                Write-Error -Message 'No valid authentication via MFA is present. Please authenticate again and retry.' -Category InvalidData
              }
              elseif ( $_.Exception.Message.contains('No valid authentication via MFA is present') ) {
                Write-Error -Message 'No valid authentication via MFA is present. Please authenticate again and retry.' -Category InvalidData
              }
              elseif ($_.Exception.Message.Contains('ExpirationRule')) {
                # Amending Schedule
                if ($Duration -eq 4) { $Duration = 1 } else { $Duration = 4 }
                Write-Warning -Message "Specified Duration is not allowed, re-trying for $Duration hour(s)"
                $end = $Date.AddHours($Duration).ToUniversalTime()
                $schedule.endDateTime = $end.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
                Write-Verbose -Message "Admin Roles will be active for $Duration hours, until: $($end.ToString())"
                $Parameters.Schedule = $schedule
                try {
                  Write-Verbose -Message "User '$Id' Role '$RoleName': Activating Role"
                  $ActivatedRole.ActiveUntil = $schedule.endDateTime
                  #$null = Open-AzureADMSPrivilegedRoleAssignmentRequest @Parameters
                  $null = New-MgPrivilegedRoleAssignmentRequest -BodyParameter $Parameters
                  [void]$ActivatedRoles.Add($ActivatedRole)
                }
                catch {
                  if ($_.Exception.Message.Contains('ExpirationRule')) {
                    Write-Error -Message 'Specified Duration is not allowed, please try again with a lower number.' -Category InvalidData
                  }
                  elseif ($_.Exception.Message.Contains('EligibilityRule')) {
                    Write-Error -Message 'User is not eligible to activate this role.' -Category InvalidData
                  }
                  elseif ($_.Exception.Message.Contains('The following policy rules failed: ["MfaRule"]')) {
                    Write-Error -Message 'No valid authentication via MFA is present. Please authenticate again and retry.' -Category InvalidData
                  }
                  else {
                    Write-Error -Message $_.Exception.Message
                  }
                }
              }
              else {
                Write-Error -Message $_.Exception.Message
              }
            }
          }
        }
      }
      #endregion
      #endregion
    }

    # Output
    if ( $PassThru ) {
      return $ActivatedRoles
    }

  } #process

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