Private/Assignment.ps1

# Assignments to roles and orgs share one implementation; the *-midPointRoleAssignment and
# *-midPointOrgAssignment cmdlets are thin wrappers that pass -Kind.
$script:midPointAssignmentKind = @{
    Role = @{ Collection = 'roles'; RefType = 'c:RoleType'; TypeName = 'RoleType'; Label = 'role' }
    Org  = @{ Collection = 'orgs'; RefType = 'c:OrgType'; TypeName = 'OrgType'; Label = 'org' }
}

function Get-midPointAssignmentRaw {
    # Reads the user (with resolveNames, so targetRef carries targetName) and returns
    # the user plus its assignments of one kind, optionally only those to one target OID.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $UserOid,
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [string] $TargetOid
    )

    $typeName = $script:midPointAssignmentKind[$Kind].TypeName
    $user = Get-midPointObjectByOid -Type users -Oid $UserOid -Options 'resolveNames'
    $assignments = @(Get-midPointProperty $user 'assignment') | Where-Object {
        $ref = Get-midPointProperty $_ 'targetRef'
        $ref -and "$(Get-midPointProperty $ref 'type')" -match "(^|[#:])$typeName$" -and
        (-not $TargetOid -or (Get-midPointProperty $ref 'oid') -eq $TargetOid)
    }
    [pscustomobject]@{
        User        = $user
        Assignments = @($assignments)
    }
}

function ConvertTo-midPointRelation {
    # 'manager' -> 'org:manager'; a value with a prefix or a full URI is kept.
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)] [string] $Relation)

    if ($Relation -match '[:#]') { return $Relation }
    "org:$Relation"
}

function Get-midPointAssignmentList {
    # Get-midPoint<Kind>Assignment: the user's assignments of one kind as module objects.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [object] $Identity,
        [object] $Target
    )

    $userOid = Resolve-midPointObjectOid -InputObject $Identity -Type users
    $targetOid = if ($null -ne $Target) { Resolve-midPointObjectOid -InputObject $Target -Type $script:midPointAssignmentKind[$Kind].Collection } else { $null }
    $data = Get-midPointAssignmentRaw -UserOid $userOid -Kind $Kind -TargetOid $targetOid
    foreach ($a in $data.Assignments) {
        ConvertTo-midPointAssignmentObject -Assignment $a -User $data.User -Kind $Kind
    }
}

function New-midPointAssignmentValue {
    # The assignment container for an 'add' delta: targetRef with type and relation, optional activation
    # from $Parameters (Relation, AdministrativeStatus, ValidFrom, ValidTo).
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory object only.')]
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [string] $TargetOid,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Parameters
    )

    $targetRef = [ordered]@{ oid = $TargetOid; type = $script:midPointAssignmentKind[$Kind].RefType }
    if ($Parameters['Relation']) { $targetRef.relation = ConvertTo-midPointRelation $Parameters['Relation'] }
    $assignment = [ordered]@{ targetRef = $targetRef }
    if ($Parameters['AdministrativeStatus']) { Set-midPointNestedValue -Target $assignment -Path 'activation/administrativeStatus' -Value (ConvertTo-midPointActivationStatus $Parameters['AdministrativeStatus']) }
    if ($Parameters.Keys -contains 'ValidFrom') { Set-midPointNestedValue -Target $assignment -Path 'activation/validFrom' -Value (ConvertTo-midPointDateTime $Parameters['ValidFrom']) }
    if ($Parameters.Keys -contains 'ValidTo') { Set-midPointNestedValue -Target $assignment -Path 'activation/validTo' -Value (ConvertTo-midPointDateTime $Parameters['ValidTo']) }
    $assignment
}

function Test-midPointAssignmentRelation {
    # True when the assignment's relation is $Relation. An empty $Relation or 'default' means the default
    # relation: no relation in targetRef, or org:default.
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [object] $Assignment,
        [AllowEmptyString()] [string] $Relation
    )

    $want = if ($Relation) { ConvertFrom-midPointQName $Relation } else { 'default' }
    $have = ConvertFrom-midPointQName (Get-midPointProperty (Get-midPointProperty $Assignment 'targetRef') 'relation')
    if (-not $have) { $have = 'default' }
    $have -eq $want
}

function Get-midPointAssignmentTargetLabel {
    # 'Some Role (oid), Other Role (oid)' for ShouldProcess.
    [CmdletBinding()]
    [OutputType([string])]
    param([object[]] $Target, [string[]] $TargetOid)

    @(for ($i = 0; $i -lt $TargetOid.Count; $i++) { "$(Get-midPointIdentityLabel $Target[$i]) ($($TargetOid[$i]))" }) -join ', '
}

function Add-midPointAssignment {
    # Add-midPoint<Kind>Assignment: one PATCH with one 'add' delta per target. $Parameters are the caller's
    # bound parameters (Identity, Relation, activation, PassThru); $Cmdlet is the caller's $PSCmdlet, so
    # -WhatIf / -Confirm apply.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Parameters,
        [Parameter(Mandatory)] [object[]] $Target,
        [Parameter(Mandatory)] [string[]] $TargetOid
    )

    $k = $script:midPointAssignmentKind[$Kind]
    $userOid = Resolve-midPointObjectOid -InputObject $Parameters['Identity'] -Type users
    $deltas = foreach ($oid in $TargetOid) {
        New-midPointItemDelta -ModificationType add -Path 'assignment' -Value (New-midPointAssignmentValue -Kind $Kind -TargetOid $oid -Parameters $Parameters)
    }

    $label = Get-midPointIdentityLabel $Parameters['Identity']
    if (-not $Cmdlet.ShouldProcess("$label ($userOid)", "Assign $($k.Label) $(Get-midPointAssignmentTargetLabel $Target $TargetOid)")) { return }

    Invoke-midPointModify -Type users -Oid $userOid -ItemDelta @($deltas)
    if ($Parameters['PassThru']) {
        Get-midPointAssignmentList -Kind $Kind -Identity $userOid | Where-Object { $TargetOid -contains $_."${Kind}Oid" }
    }
}

function Select-midPointAssignment {
    # The user's assignments of one target, narrowed to -AssignmentId when given.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [string] $UserOid,
        [Parameter(Mandatory)] [string] $TargetOid,
        [Nullable[long]] $AssignmentId
    )

    $data = Get-midPointAssignmentRaw -UserOid $UserOid -Kind $Kind -TargetOid $TargetOid
    $matching = @($data.Assignments)
    if ($null -ne $AssignmentId) {
        $matching = @($matching | Where-Object { [long](Get-midPointContainerId $_) -eq $AssignmentId })
    }
    , $matching
}

function Set-midPointAssignment {
    # Set-midPoint<Kind>Assignment: changes assignment[id]/activation of one assignment.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Parameters,
        [Parameter(Mandatory)] [object] $Target
    )

    $k = $script:midPointAssignmentKind[$Kind]
    $activationParams = Get-midPointActivationParameter -BoundParameters $Parameters
    # Validate the parameters before any request is sent.
    $null = New-midPointActivationDelta -Prefix 'activation' @activationParams
    if ($activationParams.Count -eq 0) {
        throw [System.ArgumentException]::new('Nothing to change. Give -AdministrativeStatus, -ValidFrom, -ValidTo, -ClearValidFrom or -ClearValidTo.')
    }

    $userOid = Resolve-midPointObjectOid -InputObject $Parameters['Identity'] -Type users
    $targetOid = Resolve-midPointObjectOid -InputObject $Target -Type $k.Collection
    $assignmentId = $Parameters['AssignmentId']
    $matching = Select-midPointAssignment -Kind $Kind -UserOid $userOid -TargetOid $targetOid -AssignmentId $assignmentId

    $label = Get-midPointIdentityLabel $Parameters['Identity']
    $targetLabel = Get-midPointIdentityLabel $Target
    if ($matching.Count -eq 0) {
        $record = [System.Management.Automation.ErrorRecord]::new(
            [System.Management.Automation.ItemNotFoundException]::new("User '$label' has no assignment of $($k.Label) '$targetLabel'$(if ($null -ne $assignmentId) { " with id $assignmentId" })."),
            'midPointAssignmentNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Target)
        $Cmdlet.ThrowTerminatingError($record)
    }
    if ($matching.Count -gt 1) {
        $ids = ($matching | ForEach-Object { Get-midPointContainerId $_ }) -join ', '
        throw [System.InvalidOperationException]::new("User '$label' has the $($k.Label) assigned more than once (assignment ids $ids). Select one with -AssignmentId.")
    }

    $id = Get-midPointContainerId $matching[0]
    if ($null -eq $id) { throw [System.InvalidOperationException]::new("The assignment of $($k.Label) '$targetLabel' on user '$label' has no container id.") }
    $deltas = New-midPointActivationDelta -Prefix "assignment[$id]/activation" @activationParams
    if (-not $Cmdlet.ShouldProcess("$label ($userOid)", "Change assignment $id of $($k.Label) $targetLabel")) { return }

    Invoke-midPointModify -Type users -Oid $userOid -ItemDelta $deltas
    if ($Parameters['PassThru']) {
        Get-midPointAssignmentList -Kind $Kind -Identity $userOid -Target $targetOid | Where-Object AssignmentId -EQ $id
    }
}

function Remove-midPointAssignment {
    # Remove-midPoint<Kind>Assignment: deletes the matching assignments of all targets by their container id,
    # in one PATCH.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Parameters,
        [Parameter(Mandatory)] [object[]] $Target
    )

    $k = $script:midPointAssignmentKind[$Kind]
    $assignmentId = $Parameters['AssignmentId']
    if ($null -ne $assignmentId -and $Target.Count -gt 1) {
        throw [System.ArgumentException]::new("-AssignmentId needs a single $($k.Label); $($Target.Count) were given.")
    }
    $userOid = Resolve-midPointObjectOid -InputObject $Parameters['Identity'] -Type users
    $targetOids = @(foreach ($t in $Target) { Resolve-midPointObjectOid -InputObject $t -Type $k.Collection })
    $data = Get-midPointAssignmentRaw -UserOid $userOid -Kind $Kind

    $label = Get-midPointIdentityLabel $Parameters['Identity']
    $ids = [System.Collections.Generic.List[object]]::new()
    $removed = [System.Collections.Generic.List[string]]::new()
    for ($i = 0; $i -lt $targetOids.Count; $i++) {
        $targetLabel = Get-midPointIdentityLabel $Target[$i]
        $matching = @($data.Assignments | Where-Object {
                (Get-midPointProperty (Get-midPointProperty $_ 'targetRef') 'oid') -eq $targetOids[$i] -and
                ($null -eq $assignmentId -or [long](Get-midPointContainerId $_) -eq $assignmentId)
            })
        if ($matching.Count -eq 0) {
            Write-Warning "User '$label' has no assignment of $($k.Label) '$targetLabel'$(if ($null -ne $assignmentId) { " with id $assignmentId" }). Nothing removed."
            continue
        }
        $targetIds = @($matching | ForEach-Object { Get-midPointContainerId $_ } | Where-Object { $null -ne $_ })
        if ($targetIds.Count -ne $matching.Count) { throw [System.InvalidOperationException]::new("An assignment of $($k.Label) '$targetLabel' on user '$label' has no container id.") }
        foreach ($id in $targetIds) { $ids.Add($id) }
        $removed.Add("$targetLabel (assignment $($targetIds -join ', '))")
    }
    if ($ids.Count -eq 0) { return }
    if (-not $Cmdlet.ShouldProcess("$label ($userOid)", "Remove $($k.Label) assignment(s): $($removed -join '; ')")) { return }

    $deltas = foreach ($id in $ids) {
        New-midPointItemDelta -ModificationType delete -Path 'assignment' -Value ([ordered]@{ '@id' = $id })
    }
    Invoke-midPointModify -Type users -Oid $userOid -ItemDelta @($deltas)
}

function Move-midPointAssignment {
    # Move-midPoint<Kind>Assignment: deletes the user's assignments of From (with the relation) and adds To
    # (same relation) in one PATCH. If the user already has To with that relation, only From is removed.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'ShouldProcess is called on the public cmdlet ($Cmdlet).')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('Role', 'Org')] [string] $Kind,
        [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Parameters
    )

    $k = $script:midPointAssignmentKind[$Kind]
    $from = $Parameters['From']
    $to = $Parameters['To']
    $relation = [string]$Parameters['Relation']
    $userOid = Resolve-midPointObjectOid -InputObject $Parameters['Identity'] -Type users
    $fromOid = Resolve-midPointObjectOid -InputObject $from -Type $k.Collection
    $toOid = Resolve-midPointObjectOid -InputObject $to -Type $k.Collection
    if ($fromOid -eq $toOid) { throw [System.ArgumentException]::new("-From and -To are the same $($k.Label) ($fromOid).") }

    $data = Get-midPointAssignmentRaw -UserOid $userOid -Kind $Kind
    $ofTarget = {
        param($oid)
        @($data.Assignments | Where-Object {
                (Get-midPointProperty (Get-midPointProperty $_ 'targetRef') 'oid') -eq $oid -and (Test-midPointAssignmentRelation -Assignment $_ -Relation $relation)
            })
    }
    $fromAssignments = & $ofTarget $fromOid
    $toAssignments = & $ofTarget $toOid

    $label = Get-midPointIdentityLabel $Parameters['Identity']
    $relationLabel = if ($relation) { ConvertFrom-midPointQName $relation } else { 'default' }
    if ($fromAssignments.Count -eq 0) {
        $record = [System.Management.Automation.ErrorRecord]::new(
            [System.Management.Automation.ItemNotFoundException]::new("User '$label' has no assignment of $($k.Label) '$(Get-midPointIdentityLabel $from)' with relation $relationLabel."),
            'midPointAssignmentNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $from)
        $Cmdlet.ThrowTerminatingError($record)
    }
    $ids = @($fromAssignments | ForEach-Object { Get-midPointContainerId $_ } | Where-Object { $null -ne $_ })
    if ($ids.Count -ne $fromAssignments.Count) { throw [System.InvalidOperationException]::new("An assignment of $($k.Label) '$(Get-midPointIdentityLabel $from)' on user '$label' has no container id.") }

    $deltas = [System.Collections.Generic.List[object]]::new()
    foreach ($id in $ids) { $deltas.Add((New-midPointItemDelta -ModificationType delete -Path 'assignment' -Value ([ordered]@{ '@id' = $id }))) }
    $action = "Move from $($k.Label) $(Get-midPointIdentityLabel $from) ($fromOid, assignment $($ids -join ', ')) to $(Get-midPointIdentityLabel $to) ($toOid), relation $relationLabel"
    if ($toAssignments.Count -gt 0) {
        Write-Verbose "User '$label' already has $($k.Label) '$(Get-midPointIdentityLabel $to)' with relation $relationLabel; only the assignment(s) of '$(Get-midPointIdentityLabel $from)' are removed."
        $action = "$action (already assigned: only remove)"
    }
    else {
        $value = New-midPointAssignmentValue -Kind $Kind -TargetOid $toOid -Parameters @{ Relation = $relation }
        $deltas.Add((New-midPointItemDelta -ModificationType add -Path 'assignment' -Value $value))
    }
    if (-not $Cmdlet.ShouldProcess("$label ($userOid)", $action)) { return }

    Invoke-midPointModify -Type users -Oid $userOid -ItemDelta $deltas.ToArray()
    if ($Parameters['PassThru']) { Get-midPointAssignmentList -Kind $Kind -Identity $userOid -Target $toOid }
}