internal/functions/Test-HydrationAccess.ps1
function Test-HydrationAccess { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("path", "rbacHydration", "rbacEpacDeploy", "rbacPolicyDeploy", "rbacRoleDeploy", "graphAccess", ` "internetConnection", "azureConnectivity", "azureConnection")] [string] $TestType, [Parameter(Mandatory = $false)] [string] $TestedValue, [Parameter(Mandatory = $true)] [string] $LogFilePath, [Parameter(Mandatory = $false)] [string] $RbacRestApiVersion = "2022-04-01", [Parameter(Mandatory = $false)] [string] $RbacClientId, [switch] $UseUtc = $false, [switch] $Silent ) # NOTE: (for helpfile) Managed IDs use the Obect (Principal) ID from GUI. if ($DebugPreference -eq "Continue" -or $debug) { $debug = $true $debugPreference = "Continue" Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType logEntryDataAsPresented -EntryData "Debugging Enabled for Test-HydrationAccess" -UseUtc:$UseUtc -Silent } $rolesTested = @{ Owner = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" Contributor = "b24988ac-6180-42a0-ab88-20f7382dd24c" Reader = "acdd72a7-3385-48ef-bd42-f606fba81ae7" ResourcePolicyContributor = "36243c78-bf99-498c-9df9-86d9f8d28608" RoleBasedAccessControlAdministrator = "f58310d9-a9f6-439a-9e8d-f62e7b41a168" } $testData = [ordered]@{ name = "" description = "" result = "" evaluation = "" customReturn = "" message = "" useStandardOutput = $true } if (!(Test-Path (Split-Path $LogFilePath))) { $null = New-Item -Path (Split-Path $LogFilePath) -ItemType Directory -Force } # TODO: Revisit a better way to manage the error handling here with try/catch/stop etc. to obviate the need for the error.clear() method. # Write-Warning "Service Principals are untested as of now, will be tested before release." switch ($TestType) { "path" { # Set prerequisite test information $testData.name = "Path test for $(Split-Path $TestedValue -Leaf)" $testData.description = "Confirming that the path $TestedValue exists." $testData.message = "Local path data access test: $(Split-Path $TestedValue -Leaf)" # Log test initiation Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testStart" -EntryData "$($testData.name)`: $($testData.description)" -UseUtc:$UseUtc -Silent # Add command to log file for debug purposes if ($debug) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "Test-Path $TestedValue" -UseUtc:$UseUtc } $Error.Clear() $testData.evaluation = Test-Path $TestedValue if ($Error[0].Exception.Message) { $errorMessage = $Error[0].Exception.Message Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Error - $errorMessage" -UseUtc:$UseUtc } # Evaluate Return (it is true/false binary) if ($testData.evaluation -eq $true) { # $testData.evaluation = "Passed" $testData.message = "$TestedValue exists" } else { $testData.message = "$TestedValue does not exist" } } "graphAccess" { # Set prerequisite test information $testData.name = "Azure Resource Graph Access Validation Test" $testData.description = "This will test access to Azure Resource Graph by querying for subscriptions. The presence of subscriptions in the Tenant is not a prerequisite for success." $testData.message = "Azure Resource Graph data access test" if (!($TestedValue)) { $queryString = "ResourceContainers | where type == 'microsoft.resources/subscriptions'" } else { $queryString = $TestedValue } # Log test initiation Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testStart" -EntryData "$($testData.name)`: $($testData.description)" -UseUtc:$UseUtc # Add command to log file for debug purposes if ($debug) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "Search-AzGraphAllItems -Query $queryString -ProgressItemName `"Subscriptions`"" -UseUtc:$UseUtc } # Clear error messages to clarify error output, and run test $Error.Clear() $testData.evaluation = Search-AzGraphAllItems -Query $queryString -ProgressItemName "Subscriptions" # If the return was $false or null, check for any error messages that may have occurred if (!($testData.evaluation)) { $errorMessage = $Error[0].Exception.Message | Out-Null $testData.Message = "No Subscriptions found in Tenant, verify RBAC Access of read or greater for Subscription and Management Group objects and permission to use Azure Resource Graph." } else { $testData.Message = "Azure Resource Graph test successful" } } "internetConnection" { # Set prerequisite test information if (!($TestedValue)) { $TestedValue = "www.microsoft.com" } $testData.name = "Internet Connectivity Validation Test" $testData.description = "This will test connectivity to the internet by pinging $TestedValue" $testData.Message = "Internet ping test" # Log test initiation Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testStart" -EntryData "$($testData.name)`: $($testData.description)" -UseUtc:$UseUtc -Silent:$Silent # Add command to log file for debug purposes if ($debug) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "Test-Connection -ComputerName $TestedValue -Count 3 -Quiet" -UseUtc:$UseUtc } # Clear error messages to clarify error output, and run test $Error.Clear() $testData.evaluation = Test-Connection -ComputerName $TestedValue -Count 3 -Quiet # If the return was $false or null, check for any error messages that may have occurred if (!($testData.evaluation)) { $errorMessage = $Error[0].Exception.Message | Out-Null $testData.Message = "$TestedValue ping test failed, verify ip, dns, and firewall settings." } else { $testData.Message = "$TestedValue ping test successful" } } "rbacHydration" { $rbacTest = [ordered]@{ Owner = "Skipped" Contributor = "Skipped" Reader = "Skipped" ResourcePolicyContributor = "Skipped" RoleBasedAccessControlAdministrator = "Skipped" } if (!($TestedValue)) { $scope = -join ("/providers/Microsoft.Management/managementGroups/", $(Get-AzContext).Tenant.Id) } else { $scope = -join ("/providers/Microsoft.Management/managementGroups/", $TestedValue) } Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType logEntryDataAsPresented -EntryData "Scope: $scope" -UseUtc:$UseUtc -Silent:$Silent if ($RbacClientId) { $guidPattern = '^[{(]?[0-9a-fA-F]{8}[-]?[0-9a-fA-F]{4}[-]?[0-9a-fA-F]{4}[-]?[0-9a-fA-F]{4}[-]?[0-9a-fA-F]{12}[)}]?$' if (!($RbacClientId -match $guidPattern)) { Write-Error "The ClientId provided is not a valid GUID. Please provide a valid GUID for the ClientId parameter. To use the Hydration Kit to gather this information, Run Connect-AzAccount to connect as that user, and run Get-HydrationUserObjectId to retrieve the GUID." } else { $clientId = $RbacClientId } } else { # Get ClientId from current context Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Determining account type used for current connection...." -UseUtc:$UseUtc -Silent if ($((Get-AzContext).Account.Type) -eq "ServicePrincipal") { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryasPresented" -EntryData "Account Type: ServicePrincipal" -UseUtc:$UseUtc if ($debug) { $testCommand = "(Get-AzContext).Account.Type" Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "$testCommand" -UseUtc:$UseUtc Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryasPresented" -EntryData "Account Type: ServicePrincipal" -UseUtc:$UseUtc } $clientId = (Get-AzContext).Account.Id } elseif ($((Get-AzContext).Account.Type) -eq "User") { if ($debug) { $testCommand = "Get-HydrationUserObjectId" Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "$testCommand" -UseUtc:$UseUtc Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryasPresented" -EntryData "Account Type: User" -UseUtc:$UseUtc } $Error.Clear() try { $clientId = Get-HydrationUserObjectId } catch { $errorMessage = $_.Exception.Message Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Error - $errorMessage" -UseUtc:$UseUtc Write-Error $errorMessage } } } $testData.name = "Hydration Kit RBAC Access Test" $testData.description = "Reviewing RBAC Permissions for $clientId." Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType testStart -EntryData "$($testData.name)`: $($testData.description)" -UseUtc:$UseUtc -Silent:$Silent Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "ClientId`: $clientId" -UseUtc:$UseUtc -Silent:$Silent Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Gathering RBAC entries at $scope and reviewing entries for $clientId...." -UseUtc:$UseUtc -Silent if ($debug) { $testCommand = "Get-AzRoleAssignmentsRestMethod -Scope $((Get-AzContext).Tenant.Id) -ApiVersion $RbacRestApiVersion" Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "commandStart" -EntryData "$testCommand" -UseUtc:$UseUtc -Silent:$Silent } try { $Error.Clear() $rbac = Get-AzRoleAssignmentsRestMethod -Scope $scope -ApiVersion $RbacRestApiVersion } catch { $errorMessage = $_.Exception.Message Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Error - $errorMessage" -UseUtc:$UseUtc -Silent:$Silent switch -Wildcard ($errorMessage) { "*error occurred while sending the req*" { Write-Error "An error occurred while attempting to gather RBAC data. This is generally indicative of a failed authorization attempt." } default { Write-Error $errorMessage } } return "Failed" } if ($debug) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "RBAC Data: $($rbac | ConvertTo-Json -Depth 100 -Compress) -UseUtc:$UseUtc" -Silent:$Silent } try { $Error.Clear() $rbacSubset = $rbac | Where-Object { $_.properties.principalId -eq $clientId } } catch { $errorMessage = $_.Exception.Message Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Error - $errorMessage" -UseUtc:$UseUtc -Silent:$Silent Write-Error $errorMessage return "Failed" } if ($debug) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "$clientId RBAC Data: $($rbacSubset | ConvertTo-Json -Depth 100 -Compress)" -UseUtc:$UseUtc -Silent:$Silent } foreach ($key in $rolesTested.keys) { $rbacRolesFound = $rbacSubset | Where-Object { $_.properties.roleDefinitionId -like "*$($rolesTested[$key])" } if ($rbacRolesFound) { $rbacTest.$key = "Passed" } else { $rbacTest.$key = "Failed" } Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Test for Role: $key -- $($rbacTest.$key)" -UseUtc:$UseUtc -Silent } # Test return against required configurations $rolesTested = @{ Owner = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" Contributor = "b24988ac-6180-42a0-ab88-20f7382dd24c" Reader = "acdd72a7-3385-48ef-bd42-f606fba81ae7" ResourcePolicyContributor = "36243c78-bf99-498c-9df9-86d9f8d28608" RoleBasedAccessControlAdministrator = "f58310d9-a9f6-439a-9e8d-f62e7b41a168" } if (($rbacTest.Owner -eq "Passed") -or ($rbacTest.Contributor -eq "Passed" -and $rbacTest.RoleBasedAccessControlAdministrator -eq "Passed")) { $testData.evaluation = "Passed" $testData.customReturn = "PassedHydrationDeploy" $testData.message = "This security principal has significant rights in Azure, and is overprovisioned for pipeline operations; however, is appropriate for the EPAC Hydration Kit." } elseif ($rbacTest.ResourcePolicyContributor -eq "Passed" -and $rbacTest.RoleBasedAccessControlAdministrator -eq "Passed") { $testData.evaluation = "Passed" $testData.customReturn = "PassedEpacAllDeploy" $testData.message = "This security principal can be used for both Deploy-PolicyPlan operations and Deploy-RolesPlan operations. This is appropriate for initial deployments, but is overprovisioned for pipeline use." } elseif ($rbacTest.ResourcePolicyContributor -eq "Passed") { $testData.evaluation = "Passed" $testData.customReturn = "PassedEpacPolicyDeploy" $testData.message = "This security principal should only be used for Deploy-PolicyPlan operations, and is appropriate for Pipeline Operations." } elseif ($rbacTest.RoleBasedAccessControlAdministrator -eq "Passed") { $testData.evaluation = "Passed" $testData.customReturn = "PassedEpacRoleDeploy" $testData.message = "This security principal should only be used for Deploy-RolesPlan operations, and is appropriate for Pipeline Operations." } elseif ($rbacTest.Reader -eq "Passed") { $testData.evaluation = "Passed" $testData.customReturn = "PassedEpacPlan" $testData.message = "This security principal should only be used for Build-DeploymentPlans, and is appropriate for Pipeline Operations." } else { $testData.evaluation = "Failed" $testData.customReturn = "FailedAll" $testData.message = "This security principal has insufficient rights to continue. Confirm a valid authenticated connection to Azure, confirm the tenant in use, and confirm RBAC rights for the account in use." } Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType testResult -EntryData "$($testData.name)`: $($testData.customReturn) -- $($testData.message)" -UseUtc:$UseUtc -Silent if (!($Silent)) { switch ($testData.evaluation) { "Passed" { Write-Host "$ClientId`: $($testData.message)" -ForegroundColor Green } "Failed" { Write-Host "$ClientId`: $($testData.message)" -ForegroundColor Red } default { Write-Error "Unrecognized response, please report bug in RBAC test of `$testData.Evaluation to EPAC team, value returned was $($testData.Evaluation). Please retain the log file to assist with troubleshooting." } } } } } # Anything with a custom return type will have it's own emits and log entries due to additional complexity. The final cmdlet return is the only shared aspect. if ($testData.customReturn) { $testData.result = $testData.customReturn } else { # Create standard result from standard $true/$false input if ($testData.evaluation) { $testData.result = "Passed" if (!($Silent)) { Write-Host "$($testData.message)" -ForegroundColor Green } } else { $testData.result = "Failed" # Write an error if that is desired if (!($Silent)) { Write-Error "$($testData.Message)" } # If an error message was generated during the test, it will be returned here. if ($errorMessage) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "Error: $errorMessage" -UseUtc:$UseUtc -Silent if ($debug) { Write-Error "Command Error Returned:`n $ErrorMessage" } } } # If debugging was specified, output raw returns from the test to the log file, these can be copied and pasted to be imported from json. for review as needed. if ($debug) { if ($testData.evaluation -is [string] -and !($testData.evaluation -eq [array])) { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testResult" -EntryData "$($testData.name)` -- EvaluationReturn: $($testData.evaluation)" -UseUtc:$UseUtc } else { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testResult" -EntryData "$($testData.name)` -- EvaluationReturn: $($testData.evaluation | Convertto-Json -Depth 100 -Compress)" -UseUtc:$UseUtc -Silent } } else { Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "testResult" -EntryData "$($testData.name)` -- EvaluationReturn: $($testData.evaluation)" -UseUtc:$UseUtc -Silent } } Write-HydrationLogFile -LogFilePath $LogFilePath -EntryType "logEntryDataAsPresented" -EntryData "$($testData.name)`: $($testData.result) $($testData.message)" -UseUtc:$UseUtc -Silent return $testData.result } |