source/Private/WhatIfAnalysis.ps1
|
function Test-RJTenantAlreadyConfigured { [CmdletBinding()] [OutputType([bool])] param() $allFeatures = Get-RJFeatureDefinitionList $uniqueAppIds = $allFeatures.AppId | Sort-Object -Unique foreach ($appId in $uniqueAppIds) { $servicePrincipal = Invoke-RJMgGraphCommand { Get-ServicePrincipalByAppId -AppId $appId } if ($servicePrincipal) { return $true } } return $false } function Get-RJServicePrincipalPermissionsList { [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter(Mandatory)] [string]$ServicePrincipalId, [Parameter(Mandatory)] [PSCustomObject]$MsGraphServicePrincipal ) $currentAssignments = Get-CurrentAppRoleAssignmentList -ServicePrincipalId $ServicePrincipalId $permissionDetails = @() foreach ($assignment in $currentAssignments) { if ($assignment.ResourceId -eq $MsGraphServicePrincipal.Id) { $appRole = $MsGraphServicePrincipal.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId } if ($appRole) { $permissionDetails += [PSCustomObject]@{ PermissionName = $appRole.Value AppRoleAssignmentId = $assignment.Id AppRoleId = $assignment.AppRoleId ResourceId = $assignment.ResourceId PrincipalId = $assignment.PrincipalId } } } } return ,@($permissionDetails) } function Get-RJCurrentConfiguration { [CmdletBinding()] param() Write-Verbose "Analyzing current RealmJoin configuration" $allFeatures = Get-RJFeatureDefinitionList $msGraphSp = Get-ServicePrincipalByAppId -AppId $MSGraphAppId if (-not $msGraphSp) { throw "Microsoft Graph service principal not found" } $configuredFeatures = @() $uniqueAppIds = $allFeatures | Select-Object -ExpandProperty AppId -Unique foreach ($appId in $uniqueAppIds) { $sp = Get-ServicePrincipalByAppId -AppId $appId if ($sp) { Write-Verbose "Found service principal for AppId: $appId" $permissionDetails = Get-RJServicePrincipalPermissionsList -ServicePrincipalId $sp.Id -MsGraphServicePrincipal $msGraphSp $currentPermissions = $permissionDetails | ForEach-Object { $_.PermissionName } $featuresForApp = $allFeatures | Where-Object { $_.AppId -eq $appId } foreach ($feature in $featuresForApp) { $hasAnyPermissions = $false foreach ($perm in $feature.Permissions) { if ($perm -in $currentPermissions) { $hasAnyPermissions = $true break } } if (-not $hasAnyPermissions -and $feature.ReadOnlyPermissions) { foreach ($perm in $feature.ReadOnlyPermissions) { if ($perm -in $currentPermissions) { $hasAnyPermissions = $true break } } } if ($hasAnyPermissions) { # Calculate feature-specific current permissions (intersection of SP permissions and feature permissions) $featureCurrentPermissionDetails = @() foreach ($perm in $feature.Permissions) { $permissionDetail = $permissionDetails | Where-Object { $_.PermissionName -eq $perm } if ($permissionDetail) { $featureCurrentPermissionDetails += $permissionDetail } } if ($feature.ReadOnlyPermissions) { foreach ($perm in $feature.ReadOnlyPermissions) { $permissionDetail = $permissionDetails | Where-Object { $_.PermissionName -eq $perm } if ($permissionDetail -and $permissionDetail.PermissionName -notin ($featureCurrentPermissionDetails | ForEach-Object { $_.PermissionName })) { $featureCurrentPermissionDetails += $permissionDetail } } } $configuredFeatures += [PSCustomObject]@{ Name = $feature.Name AppId = $appId ServicePrincipalId = $sp.Id ServicePrincipalName = $sp.DisplayName IsMandatory = $feature.IsMandatory CurrentPermissions = $featureCurrentPermissionDetails | ForEach-Object { $_.PermissionName } CurrentPermissionDetails = $featureCurrentPermissionDetails ExpectedPermissions = $feature.Permissions ExpectedReadOnlyPermissions = $feature.ReadOnlyPermissions } } } } } $legacyApps = @() $legacyAppList = Get-RJLegacyAppList -ErrorAction SilentlyContinue if ($legacyAppList) { foreach ($legacyApp in $legacyAppList) { $sp = Get-ServicePrincipalByAppId -AppId $legacyApp.AppId if ($sp) { $legacyApps += [PSCustomObject]@{ DisplayName = $sp.DisplayName AppId = $legacyApp.AppId ServicePrincipalId = $sp.Id } } } } return [PSCustomObject]@{ ConfiguredFeatures = $configuredFeatures LegacyApplications = $legacyApps TotalFeatures = $configuredFeatures.Count TotalLegacyApps = $legacyApps.Count MsGraphServicePrincipal = $msGraphSp } } function Get-RJOrphanedServicePrincipalsList { [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter(Mandatory)] [PSCustomObject[]]$FeatureAnalysis, [Parameter(Mandatory)] [PSCustomObject]$CurrentConfig ) $orphanedServicePrincipals = @() # Group features by AppId to check final permission state $appIdGroups = $FeatureAnalysis | Group-Object -Property AppId foreach ($appGroup in $appIdGroups) { $appId = $appGroup.Name $featuresForApp = $appGroup.Group # Get service principal info (if exists) $spInfo = $featuresForApp | Where-Object { $_.ServicePrincipalExists } | Select-Object -First 1 if ($spInfo -and $spInfo.ServicePrincipalId) { # Calculate total permissions being removed vs added for this SP $totalPermissionsToRemove = ($featuresForApp | ForEach-Object { $_.FeaturePermissionsToRemove }) | Select-Object -Unique $totalPermissionsToAdd = ($featuresForApp | ForEach-Object { $_.FeaturePermissionsToAdd }) | Select-Object -Unique # Get current SP permissions from any feature for this app $currentSPPermissions = ($CurrentConfig.ConfiguredFeatures | Where-Object { $_.AppId -eq $appId } | ForEach-Object { $_.CurrentPermissions }) | Select-Object -Unique # Calculate final permissions: current + add - remove $finalPermissions = ($currentSPPermissions + $totalPermissionsToAdd) | Where-Object { $_ -notin $totalPermissionsToRemove } | Select-Object -Unique # If no permissions remain, SP becomes orphaned if ($finalPermissions.Count -eq 0 -and $currentSPPermissions.Count -gt 0) { $orphanedServicePrincipals += [PSCustomObject]@{ AppId = $appId ServicePrincipalId = $spInfo.ServicePrincipalId ServicePrincipalName = $spInfo.ServicePrincipalName } } } } return ,@($orphanedServicePrincipals) } function Get-RJFeaturePermissionsList { [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter(Mandatory)] [PSCustomObject]$Feature, [Parameter(Mandatory)] [string]$FeatureName, [bool]$IsIncrementalMode, [string[]]$FeaturesToAdd, [PSCustomObject]$CurrentConfig, [bool]$ReadOnly ) if ($IsIncrementalMode) { # Incremental mode: use current permissions for existing features, apply ReadOnly only to new features $isNewFeature = $FeatureName -in $FeaturesToAdd $isExistingFeature = $null -ne ($CurrentConfig.ConfiguredFeatures | Where-Object { $_.Name -eq $FeatureName }) if ($isExistingFeature -and -not $isNewFeature) { # Existing feature: use current permissions from CurrentConfig $currentFeature = $CurrentConfig.ConfiguredFeatures | Where-Object { $_.Name -eq $FeatureName } return ,@($currentFeature.CurrentPermissions) } else { # New feature: apply ReadOnly logic if ($ReadOnly -and $Feature.ReadOnlyPermissions) { return ,@($Feature.ReadOnlyPermissions) } else { return ,@($Feature.Permissions) } } } else { # Traditional mode: apply ReadOnly to all features if ($ReadOnly -and $Feature.ReadOnlyPermissions) { return ,@($Feature.ReadOnlyPermissions) } else { return ,@($Feature.Permissions) } } } function Get-RJTenantPermissionAnalysis { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param( [Parameter(Mandatory)] [PSCustomObject]$FeatureResolution ) Write-Verbose "Analyzing service principal permissions for features: $($FeatureResolution.FeatureNames -join ', ')" $currentConfig = Get-RJCurrentConfiguration $msGraphSp = $currentConfig.MsGraphServicePrincipal # Build comprehensive feature analysis by checking both desired and current features $featureAnalysis = @() # Get all feature names we need to analyze (both desired and currently configured) $desiredFeatureNames = $FeatureResolution.FeatureNames $currentFeatureNames = $currentConfig.ConfiguredFeatures | ForEach-Object { $_.Name } $allFeatureNames = ($desiredFeatureNames + $currentFeatureNames) | Sort-Object -Unique foreach ($featureName in $allFeatureNames) { $feature = Get-RJFeatureByName -FeatureName $featureName if ($feature) { $isDesired = $featureName -in $desiredFeatureNames $featurePermissions = if ($isDesired) { # Get the actual resolved permissions for this feature Get-RJFeaturePermissionsList -Feature $feature -FeatureName $featureName -IsIncrementalMode $FeatureResolution.IsIncrementalMode -FeaturesToAdd $FeatureResolution.FeaturesToAdd -CurrentConfig $currentConfig -ReadOnly $FeatureResolution.ReadOnly } else { @() # Not desired, so no permissions should be assigned } # Find current feature data for this specific feature $currentFeature = $currentConfig.ConfiguredFeatures | Where-Object { $_.AppId -eq $feature.AppId -and $_.Name -eq $featureName } | Select-Object -First 1 # Check if service principal exists (any feature with same App ID) $servicePrincipalExists = $null -ne ($currentConfig.ConfiguredFeatures | Where-Object { $_.AppId -eq $feature.AppId } | Select-Object -First 1) $servicePrincipalInfo = $currentConfig.ConfiguredFeatures | Where-Object { $_.AppId -eq $feature.AppId } | Select-Object -First 1 $featureAnalysisItem = [PSCustomObject]@{ FeatureName = $featureName AppId = $feature.AppId ServicePrincipalExists = $servicePrincipalExists ServicePrincipalName = if ($servicePrincipalInfo) { $servicePrincipalInfo.ServicePrincipalName } else { "N/A" } ServicePrincipalId = if ($servicePrincipalInfo) { $servicePrincipalInfo.ServicePrincipalId } else { $null } DesiredPermissions = $featurePermissions CurrentPermissions = if ($currentFeature) { $currentFeature.CurrentPermissions } else { @() } CurrentPermissionDetails = if ($currentFeature) { $currentFeature.CurrentPermissionDetails } else { @() } FeatureChangeType = "" FeaturePermissionsToAdd = @() FeaturePermissionsToRemove = @() IsMandatory = $feature.IsMandatory IsDesired = $isDesired } if (-not $servicePrincipalExists) { if ($isDesired) { $featureAnalysisItem.FeatureChangeType = "CreateServicePrincipal" $featureAnalysisItem.FeaturePermissionsToAdd = $featurePermissions } else { # Feature not desired and SP doesn't exist - nothing to do $featureAnalysisItem.FeatureChangeType = "NoChanges" } } else { if ($isDesired) { # Feature is desired - add/modify permissions as needed $currentFeaturePermissions = if ($currentFeature) { $currentFeature.CurrentPermissions } else { @() } $currentFeaturePermissionDetails = if ($currentFeature) { $currentFeature.CurrentPermissionDetails } else { @() } $permissionsToAdd = $featurePermissions | Where-Object { $_ -notin $currentFeaturePermissions } $permissionsToRemove = $currentFeaturePermissions | Where-Object { $_ -notin $featurePermissions } # Build detailed removal objects with assignment IDs $permissionDetailsToRemove = @() foreach ($permissionName in $permissionsToRemove) { $permissionDetail = $currentFeaturePermissionDetails | Where-Object { $_.PermissionName -eq $permissionName } if ($permissionDetail) { $permissionDetailsToRemove += $permissionDetail } } $featureAnalysisItem.FeaturePermissionsToAdd = $permissionsToAdd $featureAnalysisItem.FeaturePermissionsToRemove = $permissionsToRemove $featureAnalysisItem | Add-Member -NotePropertyName "FeaturePermissionDetailsToRemove" -NotePropertyValue $permissionDetailsToRemove if ($permissionsToAdd.Count -eq 0 -and $permissionsToRemove.Count -eq 0) { $featureAnalysisItem.FeatureChangeType = "NoChanges" } elseif ($permissionsToAdd.Count -gt 0 -and $permissionsToRemove.Count -eq 0) { $featureAnalysisItem.FeatureChangeType = "AddPermissions" } elseif ($permissionsToAdd.Count -eq 0 -and $permissionsToRemove.Count -gt 0) { $featureAnalysisItem.FeatureChangeType = "RemovePermissions" } else { $featureAnalysisItem.FeatureChangeType = "ModifyPermissions" } } else { # Feature is not desired but currently configured - remove all its permissions if ($currentFeature) { $featureAnalysisItem.FeaturePermissionsToRemove = $currentFeature.CurrentPermissions $featureAnalysisItem.FeatureChangeType = "RemoveFeature" # Add detailed removal objects with assignment IDs for complete removal $featureAnalysisItem | Add-Member -NotePropertyName "FeaturePermissionDetailsToRemove" -NotePropertyValue $currentFeature.CurrentPermissionDetails } else { $featureAnalysisItem.FeatureChangeType = "NoChanges" } } } $featureAnalysis += $featureAnalysisItem } } # Detect orphaned service principals using the extracted function $orphanedServicePrincipals = Get-RJOrphanedServicePrincipalsList -FeatureAnalysis $featureAnalysis -CurrentConfig $currentConfig return @{ FeatureAnalysis = $featureAnalysis OrphanedServicePrincipals = $orphanedServicePrincipals MsGraphServicePrincipal = $msGraphSp CurrentConfiguration = $currentConfig } } function Show-RJFeatureAnalysis { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject[]]$FeatureAnalysis, [Parameter()] [PSCustomObject[]]$OrphanedServicePrincipals ) Write-Host "" Write-Host "Feature Analysis:" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Gray $featuresByAppId = $FeatureAnalysis | Group-Object -Property AppId foreach ($appGroup in $featuresByAppId) { $appId = $appGroup.Name $appFeatures = $appGroup.Group $firstFeature = $appFeatures[0] $displayName = if ($firstFeature.ServicePrincipalExists) { "($($firstFeature.ServicePrincipalName))" } else { "" } Write-Host "" Write-Host "AppId: $appId $displayName" -ForegroundColor Cyan Write-Host ("-" * 60) -ForegroundColor Gray if ($appFeatures.Count -gt 1) { $spAction = if (-not $firstFeature.ServicePrincipalExists) { "will be CREATED" } else { "will be UPDATED" } Write-Host "→ Service principal $spAction" -ForegroundColor Yellow Write-Host "" } foreach ($feature in $appFeatures) { $mandatoryText = if ($feature.IsMandatory) { " (Mandatory)" } else { "" } Write-Host " 📋 Feature: $($feature.FeatureName)$mandatoryText" -ForegroundColor White switch ($feature.FeatureChangeType) { "NoChanges" { Write-Host " ✓ No changes required" -ForegroundColor Green if ($feature.DesiredPermissions.Count -gt 0) { Write-Host " Current permissions ($($feature.DesiredPermissions.Count)): $($feature.DesiredPermissions -join ', ')" -ForegroundColor Gray } } "CreateServicePrincipal" { Write-Host " → Will assign permissions (new service principal)" -ForegroundColor Yellow if ($feature.FeaturePermissionsToAdd.Count -gt 0) { Write-Host " Permissions to assign ($($feature.FeaturePermissionsToAdd.Count)):" -ForegroundColor Yellow foreach ($permission in $feature.FeaturePermissionsToAdd) { Write-Host " + $permission" -ForegroundColor Green } } } "AddPermissions" { Write-Host " → Will add permissions" -ForegroundColor Yellow if ($feature.FeaturePermissionsToAdd.Count -gt 0) { Write-Host " Permissions to add ($($feature.FeaturePermissionsToAdd.Count)):" -ForegroundColor Yellow foreach ($permission in $feature.FeaturePermissionsToAdd) { Write-Host " + $permission" -ForegroundColor Green } } } "RemovePermissions" { Write-Host " → Will remove permissions" -ForegroundColor Yellow if ($feature.FeaturePermissionsToRemove.Count -gt 0) { Write-Host " Permissions to remove ($($feature.FeaturePermissionsToRemove.Count)):" -ForegroundColor Yellow foreach ($permission in $feature.FeaturePermissionsToRemove) { Write-Host " - $permission" -ForegroundColor Red } } } "ModifyPermissions" { Write-Host " → Will modify permissions" -ForegroundColor Yellow if ($feature.FeaturePermissionsToAdd.Count -gt 0) { Write-Host " Permissions to add ($($feature.FeaturePermissionsToAdd.Count)):" -ForegroundColor Yellow foreach ($permission in $feature.FeaturePermissionsToAdd) { Write-Host " + $permission" -ForegroundColor Green } } if ($feature.FeaturePermissionsToRemove.Count -gt 0) { Write-Host " Permissions to remove ($($feature.FeaturePermissionsToRemove.Count)):" -ForegroundColor Yellow foreach ($permission in $feature.FeaturePermissionsToRemove) { Write-Host " - $permission" -ForegroundColor Red } } } "RemoveFeature" { Write-Host " → Will remove feature" -ForegroundColor Red if ($feature.FeaturePermissionsToRemove.Count -gt 0) { Write-Host " Permissions to remove ($($feature.FeaturePermissionsToRemove.Count)):" -ForegroundColor Red foreach ($permission in $feature.FeaturePermissionsToRemove) { Write-Host " - $permission" -ForegroundColor Red } } } } if ($feature -ne $appFeatures[-1]) { Write-Host "" } } } # Show orphaned service principals if any if ($OrphanedServicePrincipals -and $OrphanedServicePrincipals.Count -gt 0) { Write-Host "" Write-Host "Orphaned Service Principals:" -ForegroundColor Magenta Write-Host ("=" * 80) -ForegroundColor Gray foreach ($orphanedSP in $OrphanedServicePrincipals) { Write-Host "→ [REMOVE] $($orphanedSP.ServicePrincipalName)" -ForegroundColor Magenta Write-Host " AppId: $($orphanedSP.AppId)" -ForegroundColor Gray Write-Host " Reason: No remaining permissions after feature removal" -ForegroundColor Gray Write-Host "" } } } function Get-RJLegacyAppDiff { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$LegacyApp ) Write-Verbose "Analyzing legacy app: $($LegacyApp.DisplayName) (AppId: $($LegacyApp.AppId))" $diff = [PSCustomObject]@{ AppId = $LegacyApp.AppId DisplayName = $LegacyApp.DisplayName ServicePrincipalExists = $false ServicePrincipalName = $null ServicePrincipalId = $null Action = "NotFound" Error = $null RequiresRemoval = $false } $servicePrincipal = Get-ServicePrincipalByAppId -AppId $LegacyApp.AppId if ($servicePrincipal) { $diff.ServicePrincipalExists = $true $diff.ServicePrincipalName = $servicePrincipal.displayName $diff.ServicePrincipalId = $servicePrincipal.id $diff.Action = "Remove" $diff.RequiresRemoval = $true Write-Verbose "Legacy app found and marked for removal: $($servicePrincipal.displayName)" } else { Write-Verbose "Legacy app not found (already removed): $($LegacyApp.DisplayName)" } Write-Verbose "Legacy app analysis complete for $($LegacyApp.AppId): Action=$($diff.Action), RequiresRemoval=$($diff.RequiresRemoval)" return $diff } function Show-RJLegacyAppAnalysis { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject[]]$Analysis ) if (-not $Analysis -or $Analysis.Count -eq 0) { return } # Only show analysis if there are Remove or Error actions $actionableApps = $Analysis | Where-Object { $_.Action -eq "Remove" -or $_.Action -eq "Error" } if ($actionableApps.Count -eq 0) { return } Write-Host "" Write-Host "Legacy Applications Analysis:" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Gray foreach ($app in $actionableApps) { switch ($app.Action) { "Remove" { Write-Host "→ [REMOVE] $($app.ServicePrincipalName)" -ForegroundColor Red Write-Host " AppId: $($app.AppId)" -ForegroundColor Gray Write-Host " ObjectId: $($app.ServicePrincipalId)" -ForegroundColor Gray } "Error" { Write-Host "✗ [ERROR] $($app.DisplayName)" -ForegroundColor Magenta Write-Host " AppId: $($app.AppId)" -ForegroundColor Gray Write-Host " Error: $($app.Error)" -ForegroundColor Red } } Write-Host "" } } |