invoke-EntraIDPimElevations.ps1

<#PSScriptInfo
.VERSION 1.4
.GUID 78e7abb6-aeae-4486-81e8-9b61e8b2e3e3
.AUTHOR JankeSkanke
.COMPANYNAME CloudWay
.COPYRIGHT 2024 CloudWay
.TAGS EntraID, PIM, Privileged Identity Management
.LICENSEURI https://github.com/JankeSkanke/EntraIDPIMElevation/blob/main/LICENSE
.PROJECTURI https://github.com/JankeSkanke/EntraIDPIMElevation
.ICONURI
.EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Users, Microsoft.Graph.Identity.Governance
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
Script now supports the following features:
    Added consent switch and cleaned up code
    Added ForceRefresh switch and user context to PowerShell Windows Title
    Added support for activating PIM Group Memberships and Ownerships
.PRIVATEDATA
#>


<#
.SYNOPSIS
    Name: invoke-EntraIDPimElevations.ps1
    Script to activate Eligible roles or Groups in Entra ID PIM for a user
.DESCRIPTION
    Script to activate Eligible roles or Grpups in Entra ID PIM for a user
.PARAMETER RolesToActivate
    Comma separated list of roles to activate, Example -RolesToActivate "Global Reader, Intune Administrator"
.PARAMETER Justification
    Justification for activating the roles
.PARAMETER ActivationDuration
    Duration in hours for the activation, default is 8 hours
.PARAMETER GetAvailableRoles
    Switch to get all available eligible roles for the user
.PARAMETER GetAvailableGroups
    Switch to get all available eligible groups for the user
.PARAMETER GroupsToActivate
    Comma separated list of groups to activate, Example -GroupsToActivate "GroupName 1, GroupName 2"
.PARAMETER AccessType
    Owner or Member for the type of group activation
.PARAMETER Consent
    Switch to request consent on all required scopes
.PARAMETER ForceRefresh
    Switch to force refresh of your logon context (tokens)
.EXAMPLE
  invoke-EntraIDPimElevations.ps1 -RolesToActivate "Global Reader, Intune Administrator" -Justification "Testing" -ActivationDuration 4
  invoke-EntraIDPimElevations.ps1 -GetAvailableRoles -ForceRefresh
  invoke-EntraIDPimElevations.ps1 -Consent
  invoke-EntraIDPimElevations.ps1 -GroupsToActivate "GroupName 1, GroupName 2" -Justification "Testing" -AccessType "Member"
 
.NOTES
    Version: 1.4
    Author: Jan Ketil Skanke (@JankeSkanke)
    Creation Date: 2023-12-13
    Author: Jan Ketil Skanke
    Contact: @JankeSkanke
    Created: 2023-12-13
    Version history:
    1.1.0 - (2023-12-13) Script Created
    1.2.0 - (2023-12-15) Added consent switch and cleaned up code
    1.3.0 . (2024-01-26) Added ForceRefresh switch and added context to Windows Title
    1.4.0 . (2024-02-17) Added support for activating PIM Group Memberships and Ownerships
#>

#Requires -Module Microsoft.Graph.Identity.Governance
#Requires -Module Microsoft.Graph.Users
#Requires -Module Microsoft.Graph.Groups

param (
    [Parameter(Mandatory=$true, ParameterSetName='ActivateRoles', HelpMessage="Specifies the roles to activate.")]
    [ValidateNotNullOrEmpty()]
    [string]$RolesToActivate,
    [Parameter(Mandatory=$true, ParameterSetName='ActivateRoles', HelpMessage="Specifies justification for activation.")]
    [Parameter(Mandatory=$true, ParameterSetName='ActivateGroups', HelpMessage="Specifies justification for activation.")]
    [ValidateNotNullOrEmpty()]
    [ValidateLength(6, 200)]
    [string]$Justification,
    [Parameter(Mandatory=$false, ParameterSetName='ActivateRoles', HelpMessage="Get all available eligible roles for the user.")]
    [Parameter(Mandatory=$false, ParameterSetName='ActivateGroups', HelpMessage="Specifies justification for activation.")]
    [Parameter(Mandatory=$false, ParameterSetName='GetRoles', HelpMessage="Get all available eligible roles for the user.")]
    [Parameter(Mandatory=$false, ParameterSetName='GetGroups', HelpMessage="Get all available eligible groups for the user.")]
    [switch]$ForceRefresh,
    [Parameter(Mandatory=$false, ParameterSetName='ActivateRoles', HelpMessage="Specifies the duration of the activation.")]
    [Parameter(Mandatory=$false, ParameterSetName='ActivateGroups', HelpMessage="Specifies justification for activation.")]
    [int]$ActivationDuration = 8,
    [Parameter(Mandatory=$true, ParameterSetName='ActivateGroups', HelpMessage="Specifies the groups to activate.")]
    [ValidateNotNullOrEmpty()]
    [string]$GroupsToActivate,
    [Parameter(Mandatory=$true, ParameterSetName='ActivateGroups', HelpMessage="Specifies owner or member for activation.")]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Owner", "Member")]
    [string]$AccessType,
    [Parameter(Mandatory=$false, ParameterSetName='GetRoles', HelpMessage="Get all available eligible roles for the user.")]
    [switch]$GetAvailableRoles,
    [Parameter(Mandatory=$false, ParameterSetName='GetGroups', HelpMessage="Get all available eligible groups for the user.")]
    [switch]$GetAvailableGroups,
    [Parameter(Mandatory=$true, ParameterSetName='Consent', HelpMessage="Request consent on scopes.")]
    [switch]$Consent
)
# Clear out any existing tokens
if ($ForceRefresh) {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

# Check if consent is requested and request it if true
if ($Consent) {
    try {
        Connect-MgGraph -Scopes "RoleEligibilitySchedule.Read.Directory, RoleAssignmentSchedule.ReadWrite.Directory, User.Read.All, GroupMember.Read.All" -NoWelcome -ErrorAction Stop
        Write-Host "Consent OK, exiting script" -ForegroundColor Green
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    exit      
    }
    catch {
        Write-Host "Consent not granted, admin request might be needed" -ForegroundColor Yellow
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        exit
    }
}

# Connect to Graph
try {
    Connect-MgGraph -NoWelcome -ErrorAction Stop 
    # Get auth context
    $MyContext = Get-MgContext
    $Me = (Get-MgUser -UserId $MyContext.Account).Id
    <#if ($MyContext.Scopes -notcontains "RoleEligibilitySchedule.Read.Directory" -or $MyContext.Scopes -notcontains "RoleAssignmentSchedule.ReadWrite.Directory" -or $MyContext.Scopes -notcontains "PrivilegedEligibilitySchedule.Read.AzureADGroup" -or $MyContext.Scopes -notcontains "User.Read.All" ) {
        Write-Warning "Missing consent, run script with -Consent switch to request consent"
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        exit
    }#>

    $host.ui.RawUI.WindowTitle = $($MyContext.Account)
    Write-Output "Connected as $($MyContext.Account)"     
}
catch {
    Write-Warning "Failed to connect to Graph: $($_.Exception.Message), exiting script"
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    exit
}

switch ($PSCmdlet.ParameterSetName) {
    "GetGroups"{
        $MyEligibleGroups = Get-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleInstance -Filter "principalId eq '$Me'" | Select-Object accessId, GroupId
        if ($($MyEligibleGroups.count) -eq 0) {
            Write-Warning "No eligible groups found, exiting script"
            break
        }
        Write-Output "You have $($MyEligibleGroups.count) eligible groups and they are:"
        foreach ($group in $MyEligibleGroups) {
            $CurrentGroup = Get-MGGroup -GroupID $group.GroupId
            Write-Output "Type: $($group.accessId) : $($CurrentGroup.DisplayName)"
        }
        break
    }
    "GetRoles"{
        # Try to get all eligible roles for user for processing
        try {
            $myRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ExpandProperty RoleDefinition -All -Filter "principalId eq '$Me'" -ErrorAction Stop
        }
        catch {
            Write-Warning "Failed to get eligible roles: $($_.Exception.Message), exiting script"
            Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
            exit
        }
        if ($($myroles.count) -eq 0) {
            Write-Warning "No eligible roles found, exiting script"
            break
        }
        Write-Output "You have $($myroles.count) eligible roles and they are:"
        Write-Output $($myRoles.RoleDefinition.DisplayName | Sort-Object | Format-Table)
        break
    }
    "ActivateRoles"{
        try {
            $myRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ExpandProperty RoleDefinition -All -Filter "principalId eq '$Me'" -ErrorAction Stop
        }
        catch {
            Write-Warning "Failed to get eligible roles: $($_.Exception.Message), exiting script"
            Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
            exit
        }
        if ($($myroles.count) -eq 0) {
            Write-Warning "No eligible roles found, exiting script"
            break
        }
        $MyRolesNames = $myRoles.RoleDefinition.DisplayName
        $Roles = $RolesToActivate.Split(",") | ForEach-Object { $_.Trim() }
        foreach ($role in $Roles) {
            if ($role -notin $MyRolesNames) {
                Write-Warning "Role $role is not eligible for you to activate, exiting script"
                exit
            }
        }
        Write-Host "You are about to activate the following roles:" -ForegroundColor Green
        Write-Output $Roles | Sort-Object | Format-Table
        Write-Output "This will be active for $($ActivationDuration) hours"
        Write-Host "Are you sure you want to continue? (Y/N)" -ForegroundColor Yellow
        $answer = Read-Host
        if ($answer -ne "Y") {
            Write-Output "Exiting script"
            break
        }
        Write-Output "Activating roles: $RolesToActivate"
        Write-Output "Reason: $Justification"
        
        # Activate the roles
        foreach ($role in $Roles) {
            $myRole = $myroles | Where-Object {$_.RoleDefinition.DisplayName -eq $role}
            #Write-Output $myRole
            $params = @{
                Action = "selfActivate"
                PrincipalId = $myRole.PrincipalId
                RoleDefinitionId = $myRole.RoleDefinitionId
                DirectoryScopeId = $myRole.DirectoryScopeId
                Justification = "Enable $role role with reason $Justification"
                ScheduleInfo = @{
                    StartDateTime = Get-Date
                    Expiration = @{
                        Type = "AfterDuration"
                        Duration = "PT$($ActivationDuration)H"
                    }
                }
            }
            try {
                $Activation = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params -ErrorAction Stop
                Write-Output "Activated role $($role) for $($ActivationDuration) hours from UTC $($Activation.ScheduleInfo.StartDateTime) and will expire at UTC $($Activation.ScheduleInfo.StartDateTime.AddHours($ActivationDuration))" 
            }
            catch {
                <#Do this if a terminating exception happens#>
                Write-Warning "Error activating role $($role): $($_.Exception.Message)"
                
            }
        }
    }
    "ActivateGroups"{
        try {
            $MyEligibleGroups = Get-MgIdentityGovernancePrivilegedAccessGroupEligibilityScheduleInstance -Filter "principalId eq '$Me'" | Select-Object accessId, GroupId -ErrorAction Stop

        }
        catch {
            Write-Warning "Failed to get eligible groups: $($_.Exception.Message), exiting script"
            Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
            exit
        }
        if ($($MyEligibleGroups.count) -eq 0) {
            Write-Warning "No eligible groups found, exiting script"
            break
        }
        $EligibleGroups = @()
        foreach ($group in $MyEligibleGroups) {
            $CurrentGroup = Get-MGGroup -GroupID $group.GroupId
            $EligibleGroups += [PSCustomObject]@{
                accessId = $group.accessId
                GroupId = $group.GroupId
                DisplayName = $CurrentGroup.DisplayName
            }
            #Write-Output "Type: $($group.accessId) : $($CurrentGroup.DisplayName)"
        }
        $Groups = $GroupsToActivate.Split(",") | ForEach-Object { $_.Trim() }
        foreach ($group in $Groups) {
            if ($group -notin $EligibleGroups.DisplayName) {
                Write-Warning "Group $group is not eligible for you to activate, exiting script"
                exit
            }
        }
        Write-Host "You are about to activate the following groups:" -ForegroundColor Green
        Write-Output $Groups | Sort-Object | Format-Table
        Write-Output "This will be active for $($ActivationDuration) hours"
        Write-Host "Are you sure you want to continue? (Y/N)" -ForegroundColor Yellow
        $answer = Read-Host
        if ($answer -ne "Y") {
            Write-Output "Exiting script"
            break
        }
        Write-Output "Activating groups: $GroupsToActivate"
        Write-Output "Reason: $Justification"
        foreach ($group in $Groups) {
            $MyGroupIdToElevate = ($EligibleGroups | Where-Object {$_.DisplayName -eq $group}).GroupId
            $accessId = ($EligibleGroups | Where-Object {$_.DisplayName -eq $group}).accessId
            $params = @{
                accessId = $accessId
                principalId = $Me
                groupId = $MyGroupIdToElevate
                action = "selfActivate"
                scheduleInfo = @{
                    startDateTime = Get-Date
                    expiration = @{
                        type = "afterDuration"
                        duration = "PT$($ActivationDuration)H"
                    }
                }
                justification = $Justification
            }
            try {
                $MyActivation = New-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleRequest -BodyParameter $params -ErrorAction Stop     
                Write-Output "Actived group $($group) $($accessId) for $($ActivationDuration) hours"
            }
            catch {
                <#Do this if a terminating exception happens#>
                Write-Output "Failed activating $($accessId) to group $($group)"
                Write-Warning "$($_.Exception.Message)"
            }
        }           
    }
    Default {exit}
}
#Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null