Private/PimRoleActivation.ps1
|
function Get-InTUIPimRequiredScopes { [CmdletBinding()] param() return @( 'RoleEligibilitySchedule.Read.Directory', 'RoleAssignmentSchedule.ReadWrite.Directory', 'RoleManagement.Read.Directory' ) } function Get-InTUIPimReauthAdditionalScopes { [CmdletBinding()] param() return @( 'DeviceManagementManagedDevices.Read.All', 'DeviceManagementConfiguration.Read.All', 'DeviceManagementApps.Read.All' ) } function Get-InTUIPimConnectionScopes { [CmdletBinding()] param() $baseScopes = @( 'DeviceManagementManagedDevices.ReadWrite.All', 'DeviceManagementManagedDevices.PrivilegedOperations.All', 'DeviceManagementApps.ReadWrite.All', 'User.Read.All', 'Group.Read.All', 'GroupMember.Read.All', 'DeviceManagementConfiguration.Read.All', 'DeviceManagementServiceConfig.Read.All', 'Directory.Read.All', 'AuditLog.Read.All', 'BitlockerKey.ReadBasic.All', 'BitlockerKey.Read.All' ) return @($baseScopes + (Get-InTUIPimRequiredScopes) + (Get-InTUIPimReauthAdditionalScopes) | Select-Object -Unique) } function Get-InTUIPimReauthScopes { [CmdletBinding()] param( [Parameter()] [string[]]$Scopes = @() ) return @($Scopes + (Get-InTUIPimConnectionScopes) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -Unique) } function Test-InTUIPimDelegatedContext { [CmdletBinding()] param( [Parameter()] [object]$Context = (Get-MgContext) ) if ($null -eq $Context) { return $false } $authType = [string]($Context.AuthType ?? '') if ($authType -match 'AppOnly|ClientCredential|ManagedIdentity') { return $false } return -not [string]::IsNullOrWhiteSpace([string]$Context.Account) } function Test-InTUIPimPermissionError { [CmdletBinding()] param( [Parameter()] [object]$ErrorInfo ) if ($null -eq $ErrorInfo) { return $false } $statusCode = [string]$ErrorInfo.StatusCode return ($statusCode -eq 'Forbidden' -or $statusCode -eq 'Unauthorized' -or $statusCode -eq '403' -or $statusCode -eq '401') -and ([string]$ErrorInfo.Uri -match '/roleManagement/directory/') } function ConvertTo-InTUIPimDuration { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateRange(1, 24)] [int]$Hours ) return "PT$($Hours)H" } function Test-InTUIPimReason { [CmdletBinding()] param( [Parameter()] [string]$Reason ) return -not [string]::IsNullOrWhiteSpace($Reason) } function ConvertTo-InTUIPimRedactedReason { [CmdletBinding()] param( [Parameter()] [string]$Reason, [Parameter()] [int]$PrefixLength = 40 ) if ([string]::IsNullOrWhiteSpace($Reason)) { return '' } $trimmed = $Reason.Trim() if ($trimmed.Length -le $PrefixLength) { return $trimmed } return "$($trimmed.Substring(0, $PrefixLength))... [redacted]" } function Get-InTUIPimRoleKey { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Role ) return "$($Role.RoleDefinitionId)|$($Role.DirectoryScopeId)|$($Role.AppScopeId)" } function Get-InTUIPimScopeLabel { [CmdletBinding()] param( [Parameter()] [string]$DirectoryScopeId ) if ([string]::IsNullOrWhiteSpace($DirectoryScopeId) -or $DirectoryScopeId -eq '/') { return 'Tenant' } return $DirectoryScopeId } function Get-InTUIPimObjectValue { [CmdletBinding()] param( [Parameter()] [object]$InputObject, [Parameter(Mandatory)] [string[]]$Name ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { foreach ($itemName in $Name) { if ($InputObject.Contains($itemName)) { return $InputObject[$itemName] } } foreach ($key in $InputObject.Keys) { foreach ($itemName in $Name) { if ([string]::Equals([string]$key, $itemName, [System.StringComparison]::OrdinalIgnoreCase)) { return $InputObject[$key] } } } } foreach ($itemName in $Name) { $property = $InputObject.PSObject.Properties[$itemName] if ($null -ne $property) { return $property.Value } } foreach ($property in $InputObject.PSObject.Properties) { foreach ($itemName in $Name) { if ([string]::Equals($property.Name, $itemName, [System.StringComparison]::OrdinalIgnoreCase)) { return $property.Value } } } $additionalProperties = $InputObject.PSObject.Properties['AdditionalProperties']?.Value if ($additionalProperties -is [System.Collections.IDictionary]) { $value = Get-InTUIPimObjectValue -InputObject $additionalProperties -Name $Name if ($null -ne $value) { return $value } } foreach ($itemName in $Name) { try { $value = $InputObject[$itemName] if ($null -ne $value) { return $value } } catch { # Some Graph SDK objects do not expose an indexer. } } return $null } function ConvertTo-InTUIPimPlainObject { [CmdletBinding()] param( [Parameter()] [object]$InputObject ) if ($null -eq $InputObject) { return $null } try { $json = $InputObject | ConvertTo-Json -Depth 20 -Compress if ([string]::IsNullOrWhiteSpace($json) -or $json -eq 'null') { return $null } return ($json | ConvertFrom-Json) } catch { return $null } } function ConvertTo-InTUIPimPlainObjectArray { [CmdletBinding()] param( [Parameter()] [object[]]$InputObject = @() ) if ($InputObject.Count -eq 0) { return @() } try { $json = @($InputObject) | ConvertTo-Json -Depth 20 -Compress if ([string]::IsNullOrWhiteSpace($json) -or $json -eq 'null') { return @() } return @($json | ConvertFrom-Json) } catch { return @() } } function Write-InTUIPimConversionDiagnostic { [CmdletBinding()] param( [Parameter()] [object[]]$Items = @(), [Parameter(Mandatory)] [string]$Source ) if ($Items.Count -eq 0) { return } $first = $Items[0] if ($null -eq $first) { Write-InTUILog -Level 'WARN' -Message 'PIM role conversion produced no usable roles' -Context @{ Source = $Source FirstItemType = 'null' PropertyNames = '' Keys = '' } return } $propertyNames = @($first.PSObject.Properties | Select-Object -ExpandProperty Name) $keys = @() if ($first -is [System.Collections.IDictionary]) { $keys = @($first.Keys) } Write-InTUILog -Level 'WARN' -Message 'PIM role conversion produced no usable roles' -Context @{ Source = $Source FirstItemType = $first.GetType().FullName PropertyNames = ($propertyNames -join ',') Keys = ($keys -join ',') } } function ConvertTo-InTUIPimRoleCollection { [CmdletBinding()] param( [Parameter()] [object[]]$Items = @(), [Parameter(Mandatory)] [string]$Source ) $roles = @($Items | ForEach-Object { ConvertTo-InTUIPimRoleItem -Schedule $_ } | Where-Object { $null -ne $_ } | Sort-Object DisplayName, DirectoryScopeId) if ($roles.Count -gt 0 -or $Items.Count -eq 0) { return $roles } $plainItems = ConvertTo-InTUIPimPlainObjectArray -InputObject $Items if ($plainItems.Count -gt 0) { $roles = @($plainItems | ForEach-Object { ConvertTo-InTUIPimRoleItem -Schedule $_ } | Where-Object { $null -ne $_ } | Sort-Object DisplayName, DirectoryScopeId) if ($roles.Count -gt 0) { return $roles } } Write-InTUIPimConversionDiagnostic -Items $Items -Source $Source return @() } function Get-InTUIPimGraphResultItems { [CmdletBinding()] param( [Parameter()] [object]$Response ) if ($null -eq $Response) { return @() } if ($Response -is [System.Collections.IDictionary]) { if ($Response.Contains('value')) { return @($Response['value']) } return @($Response) } $valueProperty = $Response.PSObject.Properties['value'] if ($null -ne $valueProperty) { return @($valueProperty.Value) } return @($Response) } function ConvertTo-InTUIPimRoleItem { [CmdletBinding()] param( [Parameter()] [object]$Schedule ) if ($null -eq $Schedule) { return $null } $principalId = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('principalId', 'PrincipalId') $roleDefinitionId = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('roleDefinitionId', 'RoleDefinitionId') if ([string]::IsNullOrWhiteSpace([string]$principalId) -or [string]::IsNullOrWhiteSpace([string]$roleDefinitionId)) { $plainSchedule = ConvertTo-InTUIPimPlainObject -InputObject $Schedule if ($null -ne $plainSchedule -and -not [object]::ReferenceEquals($plainSchedule, $Schedule)) { $principalId = Get-InTUIPimObjectValue -InputObject $plainSchedule -Name @('principalId', 'PrincipalId') $roleDefinitionId = Get-InTUIPimObjectValue -InputObject $plainSchedule -Name @('roleDefinitionId', 'RoleDefinitionId') if (-not [string]::IsNullOrWhiteSpace([string]$principalId) -and -not [string]::IsNullOrWhiteSpace([string]$roleDefinitionId)) { $Schedule = $plainSchedule } } } if ([string]::IsNullOrWhiteSpace([string]$principalId) -or [string]::IsNullOrWhiteSpace([string]$roleDefinitionId)) { return $null } $roleDefinition = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('roleDefinition', 'RoleDefinition') $roleName = (Get-InTUIPimObjectValue -InputObject $roleDefinition -Name @('displayName', 'DisplayName')) ?? (Get-InTUIPimObjectValue -InputObject $Schedule -Name @('roleDefinitionDisplayName', 'RoleDefinitionDisplayName')) ?? $roleDefinitionId ?? 'Unknown role' $scopeId = (Get-InTUIPimObjectValue -InputObject $Schedule -Name @('directoryScopeId', 'DirectoryScopeId')) ?? '/' [pscustomobject]@{ Id = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('id', 'Id') DisplayName = [string]$roleName PrincipalId = [string]$principalId RoleDefinitionId = [string]$roleDefinitionId DirectoryScopeId = [string]$scopeId AppScopeId = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('appScopeId', 'AppScopeId') StartDateTime = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('startDateTime', 'StartDateTime', 'createdDateTime', 'CreatedDateTime') EndDateTime = Get-InTUIPimObjectValue -InputObject $Schedule -Name @('endDateTime', 'EndDateTime') Source = $Schedule } } function Set-InTUIPimRoleDisplayName { [CmdletBinding()] param( [Parameter()] [object[]]$Roles = @() ) $rolesMissingNames = @($Roles | Where-Object { $_.DisplayName -eq $_.RoleDefinitionId }) if ($rolesMissingNames.Count -eq 0) { return $Roles } $definitions = @(Invoke-InTUIGraphRequest -Uri '/roleManagement/directory/roleDefinitions?$select=id,displayName' -All -NoCache) if ($definitions.Count -eq 0) { return $Roles } $displayNamesById = @{} foreach ($definition in $definitions) { $id = Get-InTUIPimObjectValue -InputObject $definition -Name @('id', 'Id') $displayName = Get-InTUIPimObjectValue -InputObject $definition -Name @('displayName', 'DisplayName') if (-not [string]::IsNullOrWhiteSpace([string]$id) -and -not [string]::IsNullOrWhiteSpace([string]$displayName)) { $displayNamesById[[string]$id] = [string]$displayName } } foreach ($role in $Roles) { if ($displayNamesById.ContainsKey($role.RoleDefinitionId)) { $role.DisplayName = $displayNamesById[$role.RoleDefinitionId] } } return $Roles } function Get-InTUIPimEligibleDirectoryRole { [CmdletBinding()] param() $uri = "/roleManagement/directory/roleEligibilityScheduleInstances/filterByCurrentUser(on='principal')?`$expand=roleDefinition&`$select=id,principalId,roleDefinitionId,directoryScopeId,appScopeId,startDateTime,endDateTime" $response = Invoke-InTUIGraphRequest -Uri $uri -Beta -All -NoCache if ($null -eq $response) { return @() } $items = @(Get-InTUIPimGraphResultItems -Response $response) $roles = @(ConvertTo-InTUIPimRoleCollection -Items $items -Source 'ScheduleInstances') Write-InTUILog -Message 'PIM eligible roles loaded' -Context @{ RawCount = $items.Count; RoleCount = $roles.Count } if ($roles.Count -gt 0) { return (Set-InTUIPimRoleDisplayName -Roles $roles) } return (Get-InTUIPimEligibleDirectoryRoleSchedule) } function Get-InTUIPimEligibleDirectoryRoleSchedule { [CmdletBinding()] param() $uri = '/roleManagement/directory/roleEligibilitySchedules?$select=id,principalId,roleDefinitionId,directoryScopeId,appScopeId,createdDateTime,status,memberType' $response = Invoke-InTUIGraphRequest -Uri $uri -All -NoCache if ($null -eq $response) { return @() } $items = @(Get-InTUIPimGraphResultItems -Response $response) $roles = @(ConvertTo-InTUIPimRoleCollection -Items $items -Source 'EligibilitySchedules') $roles = @(Set-InTUIPimRoleDisplayName -Roles $roles) Write-InTUILog -Message 'PIM eligible role schedules loaded' -Context @{ RawCount = $items.Count; RoleCount = $roles.Count } return $roles } function Get-InTUIPimActiveDirectoryRole { [CmdletBinding()] param() $uri = "/roleManagement/directory/roleAssignmentScheduleInstances/filterByCurrentUser(on='principal')?`$expand=roleDefinition&`$select=id,principalId,roleDefinitionId,directoryScopeId,appScopeId,startDateTime,endDateTime,assignmentType" $response = Invoke-InTUIGraphRequest -Uri $uri -Beta -All -NoCache if ($null -eq $response) { return @() } $items = @(Get-InTUIPimGraphResultItems -Response $response) $roles = @(ConvertTo-InTUIPimRoleCollection -Items $items -Source 'ActiveAssignments') Write-InTUILog -Message 'PIM active roles loaded' -Context @{ RawCount = $items.Count; RoleCount = $roles.Count } return $roles } function New-InTUIPimActivationRequestBody { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Role, [Parameter(Mandatory)] [ValidateRange(1, 24)] [int]$Hours, [Parameter(Mandatory)] [string]$Reason, [Parameter()] [datetime]$StartDateTime = (Get-Date).ToUniversalTime() ) if (-not (Test-InTUIPimReason -Reason $Reason)) { throw 'Activation reason is required.' } if ([string]::IsNullOrWhiteSpace([string]$Role.PrincipalId)) { throw 'PIM activation role is missing PrincipalId.' } if ([string]::IsNullOrWhiteSpace([string]$Role.RoleDefinitionId)) { throw 'PIM activation role is missing RoleDefinitionId.' } $body = @{ action = 'selfActivate' principalId = $Role.PrincipalId roleDefinitionId = $Role.RoleDefinitionId directoryScopeId = if ($Role.DirectoryScopeId) { $Role.DirectoryScopeId } else { '/' } justification = $Reason.Trim() isValidationOnly = $false scheduleInfo = @{ startDateTime = $StartDateTime.ToUniversalTime().ToString('o') expiration = @{ type = 'afterDuration' duration = ConvertTo-InTUIPimDuration -Hours $Hours } } } if ($Role.AppScopeId) { $body['appScopeId'] = $Role.AppScopeId } return $body } function New-InTUIPimDeactivationRequestBody { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Role, [Parameter()] [string]$Reason ) if ([string]::IsNullOrWhiteSpace([string]$Role.PrincipalId)) { throw 'PIM deactivation role is missing PrincipalId.' } if ([string]::IsNullOrWhiteSpace([string]$Role.RoleDefinitionId)) { throw 'PIM deactivation role is missing RoleDefinitionId.' } if ([string]::IsNullOrWhiteSpace([string]$Role.Id)) { throw 'PIM deactivation role is missing active assignment schedule Id.' } $body = @{ action = 'selfDeactivate' principalId = $Role.PrincipalId roleDefinitionId = $Role.RoleDefinitionId directoryScopeId = if ($Role.DirectoryScopeId) { $Role.DirectoryScopeId } else { '/' } isValidationOnly = $false targetScheduleId = $Role.Id } if (-not [string]::IsNullOrWhiteSpace($Reason)) { $body['justification'] = $Reason.Trim() } if ($Role.AppScopeId) { $body['appScopeId'] = $Role.AppScopeId } return $body } function Invoke-InTUIPimRoleActivation { [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Roles, [Parameter(Mandatory)] [ValidateRange(1, 24)] [int]$Hours, [Parameter(Mandatory)] [string]$Reason ) $results = [System.Collections.Generic.List[object]]::new() $redactedReason = ConvertTo-InTUIPimRedactedReason -Reason $Reason foreach ($role in @($Roles)) { $body = New-InTUIPimActivationRequestBody -Role $role -Hours $Hours -Reason $Reason Write-InTUILog -Message 'PIM activation requested' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId Hours = $Hours Reason = $redactedReason } $response = Invoke-InTUIGraphRequest -Uri '/roleManagement/directory/roleAssignmentScheduleRequests' -Method POST -Body $body -Beta if ($null -eq $response) { $errorMessage = $script:LastGraphError.Message ?? 'Graph request failed' Write-InTUILog -Level 'ERROR' -Message 'PIM activation failed' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId Error = $errorMessage Reason = $redactedReason } $results.Add([pscustomobject]@{ Role = $role RoleName = $role.DisplayName Status = 'Failed' RequestId = $null Error = $errorMessage RawResponse = $null }) continue } $status = $response.status ?? 'Submitted' Write-InTUILog -Message 'PIM activation response received' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId Status = $status RequestId = $response.id Reason = $redactedReason } $results.Add([pscustomobject]@{ Role = $role RoleName = $role.DisplayName Status = $status RequestId = $response.id Error = $null RawResponse = $response }) } return $results.ToArray() } function Invoke-InTUIPimRoleDeactivation { [CmdletBinding()] param( [Parameter(Mandatory)] [object[]]$Roles, [Parameter()] [string]$Reason ) $results = [System.Collections.Generic.List[object]]::new() $redactedReason = ConvertTo-InTUIPimRedactedReason -Reason $Reason foreach ($role in @($Roles)) { $body = New-InTUIPimDeactivationRequestBody -Role $role -Reason $Reason Write-InTUILog -Message 'PIM deactivation requested' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId TargetScheduleId = $role.Id Reason = $redactedReason } $response = Invoke-InTUIGraphRequest -Uri '/roleManagement/directory/roleAssignmentScheduleRequests' -Method POST -Body $body -Beta if ($null -eq $response) { $errorMessage = $script:LastGraphError.Message ?? 'Graph request failed' Write-InTUILog -Level 'ERROR' -Message 'PIM deactivation failed' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId TargetScheduleId = $role.Id Error = $errorMessage Reason = $redactedReason } $results.Add([pscustomobject]@{ Role = $role RoleName = $role.DisplayName Status = 'Failed' RequestId = $null Error = $errorMessage RawResponse = $null }) continue } $status = $response.status ?? 'Submitted' Write-InTUILog -Message 'PIM deactivation response received' -Context @{ RoleName = $role.DisplayName RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId TargetScheduleId = $role.Id Status = $status RequestId = $response.id Reason = $redactedReason } $results.Add([pscustomobject]@{ Role = $role RoleName = $role.DisplayName Status = $status RequestId = $response.id Error = $null RawResponse = $response }) } return $results.ToArray() } |