Providers/FreeIPA/Public/New-FreeIPAAutomemberRule.ps1

function New-FreeIPAAutomemberRule {
    <#
    .EXTERNALHELP TestEnvironment-Help.xml
    .SYNOPSIS
        Creates the seeded automember rules from Data\FreeIPAAutomemberRules.csv and rebuilds the seeded entries against them
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [string[]]$Target,

        [Parameter()]
        [switch]$SkipRebuild,

        [Parameter()]
        [switch]$PassThru
    )

    $connection = Get-FreeIPAConnection
    $marker = Get-FreeIPASeedMarker -Connection $connection

    $csvPath = Join-Path -Path (Get-FreeIPADataPath) -ChildPath 'FreeIPAAutomemberRules.csv'
    $rows = @(Import-Csv -Path $csvPath -Encoding UTF8)
    if ($Target) {
        $rows = @($rows | Where-Object { $Target -contains $_.Target })
        $unknown = @($Target | Where-Object { $rows.Target -notcontains $_ })
        if ($unknown) { throw "No definition in $csvPath for: $($unknown -join ', ')" }
    }

    $result = [PSCustomObject]@{
        TotalRules        = $rows.Count
        CreatedRules      = 0
        UpdatedRules      = 0
        ConditionsApplied = 0
        EntriesRebuilt    = 0
        Rules             = @()
        Errors            = @()
    }

    $split = { param($value) @([string]$value -split ';' | Where-Object { $_ }) }
    $first = { param($value) if ($value -is [array]) { if ($value.Count -gt 0) { [string]$value[0] } else { '' } } else { [string]$value } }

    $existing = @{}
    foreach ($entry in (Get-FreeIPASeededObject -Type AutomemberRules -Connection $connection)) { $existing['{0}/{1}' -f $entry.automembertype, (& $first $entry.cn)] = $entry }

    $rules = [System.Collections.Generic.List[object]]::new()
    foreach ($row in $rows) {
        $name = Resolve-FreeIPASeedName -Key $row.Target -Marker $marker -Connection $connection
        $type = $row.Kind
        if (-not $PSCmdlet.ShouldProcess("$name ($type)", 'Create FreeIPA automember rule')) { continue }
        try {
            $options = @{ type = $type; description = ('{0} {1}' -f $row.Description, $marker.Marker).Trim() }
            if ($existing.ContainsKey("$type/$name")) {
                $null = Invoke-FreeIPARequest -Method 'automember_mod' -Arguments $name -Options $options -Connection $connection -IgnoreError 'EmptyModlist'
                $result.UpdatedRules++
            }
            else {
                $null = Invoke-FreeIPARequest -Method 'automember_add' -Arguments $name -Options $options -Connection $connection
                $result.CreatedRules++
                Write-Verbose "Created automember rule $name"
            }

            # One condition per call. A leading '!' is an exclusive regex; anything else is
            # inclusive. A condition that already exists is the normal answer on a re-run.
            foreach ($condition in (& $split $row.Conditions)) {
                $exclusive = $condition.StartsWith('!')
                $attribute, $pattern = $condition.TrimStart('!') -split '=', 2
                $conditionOptions = @{ type = $type; key = $attribute }
                if ($exclusive) { $conditionOptions['automemberexclusiveregex'] = [object[]]@($pattern) }
                else { $conditionOptions['automemberinclusiveregex'] = [object[]]@($pattern) }
                $outcome = Invoke-FreeIPARequest -Method 'automember_add_condition' -Arguments $name -Options $conditionOptions -Connection $connection
                if ($outcome -and $outcome.PSObject.Properties['completed']) { $result.ConditionsApplied += [int]$outcome.completed }
                foreach ($failure in @(Get-FreeIPAMemberFailure -Outcome $outcome)) {
                    if ($failure -like '*already exists*') { continue }
                    $message = "Could not add a condition to '$name': $failure"
                    $result.Errors += $message
                    Write-Error $message
                }
            }

            $rules.Add([PSCustomObject]@{ Key = $row.Target; Name = $name; Type = $type; Conditions = @(& $split $row.Conditions) })
        }
        catch {
            $message = "Failed to create automember rule '$name': $($_.Exception.Message)"
            $result.Errors += $message
            Write-Error $message
        }
    }

    # The rebuild, scoped to the module's own entries by name and in batches. A rebuild
    # without a list would walk every user or host in the realm.
    if (-not $SkipRebuild -and $rules.Count -gt 0) {
        $rebuilds = @()
        if (@($rules | Where-Object Type -eq 'group').Count -gt 0) {
            $logins = @(Get-FreeIPASeededObject -Type Users -Connection $connection | ForEach-Object { & $first $_.uid })
            $rebuilds += @{ Type = 'group'; Option = 'users'; Names = $logins }
        }
        if (@($rules | Where-Object Type -eq 'hostgroup').Count -gt 0) {
            $fqdns = @(Get-FreeIPASeededObject -Type Hosts -Connection $connection | ForEach-Object { & $first $_.fqdn })
            $rebuilds += @{ Type = 'hostgroup'; Option = 'hosts'; Names = $fqdns }
        }
        foreach ($rebuild in $rebuilds) {
            $names = @($rebuild.Names)
            if ($names.Count -eq 0) { continue }
            if (-not $PSCmdlet.ShouldProcess("$($names.Count) seeded $($rebuild.Option)", 'Rebuild automember membership')) { continue }
            for ($start = 0; $start -lt $names.Count; $start += 100) {
                $chunk = @($names[$start..([Math]::Min($start + 99, $names.Count - 1))])
                try {
                    $rebuildOptions = @{ type = $rebuild.Type }
                    $rebuildOptions[$rebuild.Option] = [object[]]$chunk
                    $null = Invoke-FreeIPARequest -Method 'automember_rebuild' -Options $rebuildOptions -Connection $connection
                    $result.EntriesRebuilt += $chunk.Count
                }
                catch {
                    $message = "Failed to rebuild $($chunk.Count) $($rebuild.Option): $($_.Exception.Message)"
                    $result.Errors += $message
                    Write-Error $message
                }
            }
        }
    }

    $result.Rules = $rules.ToArray()
    Write-Verbose "Automember: $($result.CreatedRules) rules created, $($result.UpdatedRules) updated, $($result.ConditionsApplied) conditions, $($result.EntriesRebuilt) entries rebuilt, $($result.Errors.Count) problems"
    if ($PassThru) { return $result }
}