Providers/FreeIPA/Public/New-FreeIPAGroup.ps1
|
function New-FreeIPAGroup { <# .EXTERNALHELP TestEnvironment-Help.xml .SYNOPSIS Creates the seeded FreeIPA groups, nested as Data\FreeIPAGroups.csv describes #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param( [Parameter()] [string[]]$GroupName, [Parameter()] [ValidateSet('Core', 'Bulk')] [string[]]$Tier, [Parameter()] [switch]$ShowProgress, [Parameter()] [switch]$PassThru ) $connection = Get-FreeIPAConnection $marker = Get-FreeIPASeedMarker -Connection $connection $csvPath = Join-Path -Path (Get-FreeIPADataPath) -ChildPath 'FreeIPAGroups.csv' $allRows = @(Import-Csv -Path $csvPath -Encoding UTF8) $rows = $allRows if ($Tier) { $rows = @($rows | Where-Object { $Tier -contains $_.Tier }) } if ($GroupName) { $rows = @($allRows | Where-Object { $GroupName -contains $_.Name }) $unknown = @($GroupName | Where-Object { $rows.Name -notcontains $_ }) if ($unknown) { throw "No definition in $csvPath for: $($unknown -join ', ')" } } # Depth from the full CSV, not the selection, so a partial rebuild still creates a parent # before its child when both are selected. A group can name more than one parent, so the # depth is the longest path up, and the walk is bounded in case the CSV ever holds a cycle. $parentsOf = @{} foreach ($row in $allRows) { $parentsOf[$row.Name] = @($row.Parent -split ';' | Where-Object { $_ }) } $depthOf = { param($key) $depth = 0 $frontier = @($parentsOf[$key]) while ($frontier.Count -gt 0 -and $depth -lt 20) { $depth++ $frontier = @($frontier | ForEach-Object { if ($parentsOf.ContainsKey($_)) { $parentsOf[$_] } }) } $depth } $rows = @($rows | Sort-Object -Property @{ Expression = { & $depthOf $_.Name } }, Name) $result = [PSCustomObject]@{ TotalGroups = $rows.Count CreatedGroups = 0 UpdatedGroups = 0 NestingsApplied = 0 ManagersApplied = 0 ManagersDeferred = 0 Groups = @() Errors = @() } $existing = @{} foreach ($group in (Get-FreeIPASeededObject -Type Groups -Connection $connection)) { $existing[[string](@($group.cn)[0])] = $group } $split = { param($value) @([string]$value -split ';' | Where-Object { $_ }) } $groups = [System.Collections.Generic.List[object]]::new() $created = @{} $index = 0 foreach ($row in $rows) { $name = Resolve-FreeIPASeedName -Key $row.Name -Marker $marker -Connection $connection $index++ Write-TestProgress -Activity 'Seeding groups' -Status "$index of $($rows.Count): $name" ` -PercentComplete ([int](100 * $index / [Math]::Max(1, $rows.Count))) -ShowProgress:$ShowProgress if (-not $PSCmdlet.ShouldProcess($name, 'Create FreeIPA group')) { continue } try { $description = ('{0} {1}' -f $row.Description, $marker.Marker).Trim() if ($existing.ContainsKey($name)) { $null = Invoke-FreeIPARequest -Method 'group_mod' -Arguments $name -Connection $connection ` -Options @{ description = $description } -IgnoreError 'EmptyModlist' $result.UpdatedGroups++ Write-Verbose "Updated group $name" } else { $options = @{ description = $description } switch ($row.Type) { 'nonposix' { $options['nonposix'] = $true } 'external' { $options['external'] = $true } default { if ($row.GidNumber -match '^\d+$') { $options['gidnumber'] = [int]$row.GidNumber } } } $null = Invoke-FreeIPARequest -Method 'group_add' -Arguments $name -Options $options -Connection $connection $result.CreatedGroups++ Write-Verbose "Created group $name" } $created[$row.Name] = $name $groups.Add([PSCustomObject]@{ Key = $row.Name Name = $name Type = $row.Type Parents = @($parentsOf[$row.Name]) Category = $row.Category }) } catch { $message = "Failed to create group '$name': $($_.Exception.Message)" $result.Errors += $message Write-Error $message } } Write-TestProgress -Activity 'Seeding groups' -Completed -ShowProgress:$ShowProgress # Nesting, one call per parent, naming every child that exists. A parent outside the # selection is fine if it already exists in the realm; one that does not is skipped. $childrenOf = @{} foreach ($row in $rows) { if (-not $created.ContainsKey($row.Name)) { continue } foreach ($parentKey in $parentsOf[$row.Name]) { $parentName = Resolve-FreeIPASeedName -Key $parentKey -Marker $marker -Connection $connection if (-not ($created.ContainsKey($parentKey) -or $existing.ContainsKey($parentName))) { Write-Warning "Group '$($created[$row.Name])' names parent '$parentKey', which does not exist. Left unnested." continue } if (-not $childrenOf.ContainsKey($parentName)) { $childrenOf[$parentName] = [System.Collections.Generic.List[string]]::new() } $childrenOf[$parentName].Add($created[$row.Name]) } } foreach ($parentName in ($childrenOf.Keys | Sort-Object)) { if (-not $PSCmdlet.ShouldProcess($parentName, "Nest $($childrenOf[$parentName].Count) member group(s)")) { continue } try { $outcome = Invoke-FreeIPARequest -Method 'group_add_member' -Arguments $parentName -Connection $connection ` -Options @{ group = [object[]]@($childrenOf[$parentName]) } $result.NestingsApplied += [int]$outcome.completed foreach ($failure in @(Get-FreeIPAMemberFailure -Outcome $outcome)) { if ($failure -like '*already a member*') { continue } $message = "Could not nest under '$parentName': $failure" $result.Errors += $message Write-Error $message } } catch { $message = "Failed to nest under '$parentName': $($_.Exception.Message)" $result.Errors += $message Write-Error $message } } # Member managers: the users and groups who may change the membership without being # administrators. The groups are applied here, after every group exists; the users are # not, because the groups are seeded before the users - New-FreeIPAUser applies those # once the users are there, and they are counted here as deferred. foreach ($row in $rows) { if (-not $created.ContainsKey($row.Name)) { continue } $result.ManagersDeferred += @(& $split $row.ManagerUsers).Count $managers = @{ user = @() group = @(& $split $row.ManagerGroups | ForEach-Object { Resolve-FreeIPASeedName -Key $_ -Marker $marker -Connection $connection }) } if (($managers.user.Count + $managers.group.Count) -eq 0) { continue } $name = $created[$row.Name] if (-not $PSCmdlet.ShouldProcess($name, "Add $($managers.user.Count + $managers.group.Count) member manager(s)")) { continue } $added = Add-FreeIPAMember -Method 'group_add_member_manager' -Name $name -Members $managers -Connection $connection $result.ManagersApplied += $added.Completed foreach ($problem in $added.Errors) { $result.Errors += "Group '$name': $problem" Write-Error "Group '$name': $problem" } } $result.Groups = $groups.ToArray() Write-Verbose ("Groups: $($result.CreatedGroups) created, $($result.UpdatedGroups) updated, " + "$($result.NestingsApplied) nestings, $($result.Errors.Count) problems") if ($PassThru) { return $result } } |