Providers/FreeIPA/Public/Test-FreeIPAEnvironment.ps1
|
function Test-FreeIPAEnvironment { <# .SYNOPSIS Verifies that the seeded FreeIPA realm matches the seed data .DESCRIPTION Reads the users, groups and hosts the module owns in the connected realm, the same way teardown finds them, and compares them with the seed files: every seeded user, staged user, preserved user, group and host should be present, nothing the module owns should be there that the data does not describe, every user's display name should match the data by codepoint, and every group a user row lists and every hostgroup a host row lists should hold its member. Names are compared ordinally, not with -eq, because a decomposed and a precomposed name are equal to -eq and different on the wire; a name that came back mangled is the fault this exists to catch. Logins and host names are compared case-insensitively because the realm folds them. Memberships are judged on what is missing only: an automember rule adds members the data never lists, and that is not a fault. Every other seeded type is counted and reported without a verdict, because its rows do not map one to one onto objects. .PARAMETER SkipMembership Do not compare group and hostgroup memberships .PARAMETER Quiet Return the result without writing to the console .OUTPUTS PSCustomObject typed TestEnvironmentVerification. Passed is $true when every check passed. .EXAMPLE PS> Test-FreeIPAEnvironment Prints one line per check and returns the result. .EXAMPLE PS> (Test-FreeIPAEnvironment -Quiet).Checks | Where-Object { $_.Passed -eq $false } Lists only the checks that failed, with their missing and unexpected names. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [switch]$SkipMembership, [Parameter()] [switch]$Quiet ) $connection = Get-FreeIPAConnection $marker = Get-FreeIPASeedMarker -Connection $connection $dataPath = Get-FreeIPADataPath $checks = New-Object System.Collections.Generic.List[object] $read = { param($file) @(Import-Csv -Path (Join-Path -Path $dataPath -ChildPath $file) -Encoding UTF8) } # FreeIPA returns every attribute as a list, even a single-valued one. $first = { param($value) if ($value -is [array]) { if ($value.Count -gt 0) { [string]$value[0] } else { '' } } elseif ($null -eq $value) { '' } else { [string]$value } } $nameOf = { param($key) Resolve-FreeIPASeedName -Key $key -Marker $marker -Connection $connection } $hostOf = { param($key) Resolve-FreeIPASeedName -Key $key -Kind Host -Marker $marker -Connection $connection } # --- Users, by lifecycle --------------------------------------------------------------- # A user's uid is its row's username as written, with no prefix: the seed prefixes the # names it invents - groups, hosts, rules - and leaves logins alone, the same as Authentik. $userRows = & $read 'FreeIPAUsers.csv' $activeRows = @($userRows | Where-Object { $_.Lifecycle -notin 'Staged', 'Preserved' }) $stagedRows = @($userRows | Where-Object { $_.Lifecycle -eq 'Staged' }) $preservedRows = @($userRows | Where-Object { $_.Lifecycle -eq 'Preserved' }) $users = @(Get-FreeIPASeededObject -Type Users -Detail -Connection $connection) $staged = @(Get-FreeIPASeededObject -Type StagedUsers -Connection $connection) $preserved = @(Get-FreeIPASeededObject -Type PreservedUsers -Connection $connection) $checks.Add((New-TestEnvironmentCheck -Name 'Users' ` -Expected @($activeRows | ForEach-Object { $_.Username }) ` -Found @($users | ForEach-Object { & $first $_.uid }) -IgnoreCase)) $checks.Add((New-TestEnvironmentCheck -Name 'Staged users' ` -Expected @($stagedRows | ForEach-Object { $_.Username }) ` -Found @($staged | ForEach-Object { & $first $_.uid }) -IgnoreCase)) $checks.Add((New-TestEnvironmentCheck -Name 'Preserved users' ` -Expected @($preservedRows | ForEach-Object { $_.Username }) ` -Found @($preserved | ForEach-Object { & $first $_.uid }) -IgnoreCase)) $userByUid = @{} foreach ($user in $users) { $uid = & $first $user.uid if ($uid) { $userByUid[$uid.ToLowerInvariant()] = $user } } $compared = 0 $mismatch = foreach ($row in $activeRows) { $user = $userByUid[([string]$row.Username).ToLowerInvariant()] if (-not $user) { continue } $compared++ $actual = & $first $user.displayname if (-not [string]::Equals($actual, [string]$row.DisplayName, [StringComparison]::Ordinal)) { "{0}: '{1}' should be '{2}'" -f $row.Username, $actual, $row.DisplayName } } $checks.Add((New-TestEnvironmentCheck -Name 'User display names' -Compared $compared -Mismatch @($mismatch))) # --- Groups and hosts ------------------------------------------------------------------ $groupRows = & $read 'FreeIPAGroups.csv' $groups = @(Get-FreeIPASeededObject -Type Groups -Connection $connection) $checks.Add((New-TestEnvironmentCheck -Name 'Groups' ` -Expected @($groupRows | ForEach-Object { & $nameOf $_.Name }) ` -Found @($groups | ForEach-Object { & $first $_.cn }) -IgnoreCase)) $hostRows = & $read 'FreeIPAHosts.csv' $hosts = @(Get-FreeIPASeededObject -Type Hosts -Detail -Connection $connection) $checks.Add((New-TestEnvironmentCheck -Name 'Hosts' ` -Expected @($hostRows | ForEach-Object { & $hostOf $_.Name }) ` -Found @($hosts | ForEach-Object { & $first $_.fqdn }) -IgnoreCase)) # --- Memberships ----------------------------------------------------------------------- if (-not $SkipMembership) { $expectedPairs = New-Object System.Collections.Generic.List[string] $foundPairs = New-Object System.Collections.Generic.List[string] foreach ($row in $activeRows) { $uid = [string]$row.Username foreach ($key in @($row.Groups -split ';' | Where-Object { $_ })) { $expectedPairs.Add(('{0} <- {1}' -f (& $nameOf $key), $uid)) } } foreach ($user in $users) { $uid = & $first $user.uid foreach ($groupName in @($user.memberof_group)) { if ($groupName) { $foundPairs.Add(('{0} <- {1}' -f [string]$groupName, $uid)) } } } $checks.Add((New-TestEnvironmentCheck -Name 'Group memberships' -Expected $expectedPairs -Found $foundPairs -IgnoreCase -MissingOnly)) $expectedHostPairs = New-Object System.Collections.Generic.List[string] $foundHostPairs = New-Object System.Collections.Generic.List[string] foreach ($row in $hostRows) { $fqdn = & $hostOf $row.Name foreach ($key in @($row.Hostgroups -split ';' | Where-Object { $_ })) { $expectedHostPairs.Add(('{0} <- {1}' -f (& $nameOf $key), $fqdn)) } } foreach ($seededHost in $hosts) { $fqdn = & $first $seededHost.fqdn foreach ($hostgroupName in @($seededHost.memberof_hostgroup)) { if ($hostgroupName) { $foundHostPairs.Add(('{0} <- {1}' -f [string]$hostgroupName, $fqdn)) } } } $checks.Add((New-TestEnvironmentCheck -Name 'Hostgroup memberships' -Expected $expectedHostPairs -Found $foundHostPairs -IgnoreCase -MissingOnly)) } # --- Everything else, counted ---------------------------------------------------------- foreach ($type in 'Hostgroups', 'Netgroups', 'HbacServices', 'HbacServiceGroups', 'HbacRules', 'SudoCommands', 'SudoCommandGroups', 'SudoRules', 'Permissions', 'Privileges', 'Roles', 'PasswordPolicies', 'Services', 'ServiceDelegationRules', 'ServiceDelegationTargets', 'IdViews', 'OtpTokens', 'AutomemberRules', 'AutomountLocations', 'SelinuxUserMaps', 'CertMapRules', 'CaAcls', 'Certificates', 'DnsZones', 'DnsRecords', 'RadiusProxies', 'IdentityProviders') { try { $count = @(Get-FreeIPASeededObject -Type $type -Connection $connection).Count $checks.Add((New-TestEnvironmentCheck -Name $type -FoundCount $count)) } catch { Write-Warning "Could not count the seeded $type`: $($_.Exception.Message)" } } return New-TestEnvironmentVerification -Provider 'FreeIPA' -Target $connection.BaseUrl -Check $checks.ToArray() -Quiet:$Quiet } |