Public/Metaverse/Set-JIMMetaverseObjectPassword.ps1
|
# Copyright (c) Tetron Limited. All rights reserved. # Licensed under the Tetron Commercial License. See LICENSE file in the project root. function Set-JIMMetaverseObjectPassword { <# .SYNOPSIS Sets a person's password, on the accounts you name or on every Connected System configured for Password Synchronisation. .DESCRIPTION The one command for giving somebody a password, aimed one of two ways: Named accounts. Pass -ConnectedSystemId to set the password on this person's accounts in those Connected Systems. This is the reset case: you chose the password for them. By default it expires at next sign-in, and the command waits up to ten seconds to tell you what each account did with it. An account in a system whose Password Synchronisation is switched off is still delivered to; you named it. -EnableAccount is available in this mode only. Every configured system. Omit -ConnectedSystemId and the password goes to every Connected System configured for Password Synchronisation in which the person has an account, including systems that are switched off (held until switched on) and systems where the account does not exist yet (delivered once it does). This is the event case: the person's password changed somewhere, and the rest should hold it. By default expiry is left to each system's own policy, since a password the person chose should not demand they choose another, and the command returns as soon as the change is recorded. To set a password on every account the person has regardless of configuration, name the systems: -ConnectedSystemId (Get-JIMMetaverseObject -Id $id).ConnectedSystemObjects.ConnectedSystemId Either way JIM queues one change per Connected System, encrypted, and the Password Delivery Service makes the first attempt within about a second, whatever the synchronisation engine is doing. A system that is unavailable delays the password rather than losing it: JIM retries on its own clock, and a system that refuses the password parks it for you to look at rather than retrying into the same refusal. The password is held only until it is delivered; a copy a system refused is kept, still encrypted, so JIM can finish the job once the cause is dealt with, until the change expires or retention removes it. It is never logged, never returned, and never recorded on an Activity. Supply the password with -Password, or use -Generate to have JIM produce one that satisfies the discovered policy of every Connected System the person has an account in. A generated password is returned once, on the result's GeneratedPassword property; JIM holds it only until it is delivered and cannot give it to you again. Each target's State says where its password got to: Set, Retrying (with NextAttemptAt), Parked (with the system's own Message), Held behind a switched-off system, or Queued and Delivering while still in flight. A Parked target is reported as a non-terminating error as well, so a script that stops on errors stops on a refusal; the result is written to the pipeline first either way. .PARAMETER Id The unique identifier (GUID) of the Metaverse Object whose password this is. .PARAMETER ConnectedSystemId The Connected Systems to set the password in. The person must have an account in every one named; the command refuses, and sets nothing, where they do not. Omit it to propagate the password to every Connected System configured for Password Synchronisation instead. .PARAMETER Password The password, as a SecureString. Encrypted before JIM stores it and held only until delivered; never logged, never returned, and never recorded on an Activity. .PARAMETER Generate Has JIM generate one password that satisfies every Connected System the person has an account in, instead of you supplying one. With -ConnectedSystemId, only the systems named count. JIM reconciles their discovered policies (the longest minimum length any of them demands, and only the character categories all of them count) and refuses outright where no single password can satisfy them all, rather than handing back one accepted by the first system and refused by the second. A system JIM could read no policy from is reported as a warning, because the password is about to go there and JIM cannot promise it will be accepted. The generated password is returned on the result's GeneratedPassword property as a SecureString. That is the only chance to capture it. .PARAMETER ExpiryBehaviour What happens to the password once each target has it. Valid values: RequireChangeAtNextSignIn, ExpiresAccordingToTargetPolicy, NeverExpires. Defaults to RequireChangeAtNextSignIn with -ConnectedSystemId (somebody else chose this password) and to ExpiresAccordingToTargetPolicy without it (the person chose it, and should not be made to choose another). .PARAMETER EnableAccount Enables the named accounts as part of setting the password. Omit it to leave their enabled state untouched. Only available with -ConnectedSystemId: a password propagated to every configured system never enables an account, because it reaches accounts an administrator may have disabled on purpose. .PARAMETER Wait How many seconds, from 0 to 30, to wait for the systems to answer before returning. Defaults to 10 with -ConnectedSystemId and to 0 without it. A wait ends early once every target has settled; a target still Queued or Delivering when it runs out is reported as such, with Settled false, and its outcome appears on the Activity and the person's Password tab once it lands. The ceiling is the API's; a script that needs to watch for longer should poll Get-JIMPendingPasswordChange -MetaverseObjectId instead. .PARAMETER Force Skips the confirmation prompt. .OUTPUTS One PSCustomObject describing the change: | Property | Description | |--------------------|--------------------------------------------------------------------------------------| | ActivityId | The Activity recording the change; its children hold each system's outcome | | Origin | Explicit (accounts named) or Propagated (every configured system) | | Settled | Whether every target had reached an outcome you need not wait on when this returned | | QueuedForNoSystems | True when a propagated change found no configured system; nothing was queued | | Targets | One entry per Connected System, described below | | GeneratedPassword | The password JIM produced, as a SecureString; present only with -Generate | Each entry under Targets: | Property | Description | |-------------------------|---------------------------------------------------------------------------------| | ConnectedSystemId | The Connected System | | ConnectedSystemName | Its name | | ConnectedSystemObjectId | The account, or null where the person has no account in this system yet | | Enabled | Whether the system is taking propagated passwords; false means Held | | State | Queued, Delivering, Set, Retrying, Parked, Held, Expired or Cancelled | | NextAttemptAt | When the next attempt falls due, for a Retrying target | | Message | The system's own words on its most recent outcome | | AttemptCount | How many delivery attempts this system has had | | FailureReason | Why the last attempt failed; empty before an attempt or once set | No property carries the password you supplied. .EXAMPLE $password = Read-Host -AsSecureString "New password" $result = Set-JIMMetaverseObjectPassword -Id 8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f -ConnectedSystemId 1,2 -Password $password $result.Targets | Select-Object ConnectedSystemName, State, Message Sets one password on this person's accounts in Connected Systems 1 and 2, requiring a change at the next sign-in, waits up to ten seconds, and shows what each system did with it. .EXAMPLE $result = Set-JIMMetaverseObjectPassword -Id $id -ConnectedSystemId 3 -Generate -EnableAccount -Force ConvertFrom-SecureString -SecureString $result.GeneratedPassword -AsPlainText Has JIM produce a password the named system will accept, sets it, enables the account, and reads back what was used. Capture it: this is the only chance to. .EXAMPLE $password = Read-Host -AsSecureString "New password" Set-JIMMetaverseObjectPassword -Id 8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f -Password $password -Force Records that the person's password changed and queues it for every Connected System configured for Password Synchronisation, returning as soon as it is recorded. .EXAMPLE $result = Set-JIMMetaverseObjectPassword -Id $id -Password $password -Wait 10 -Force if (-not $result.Settled) { Write-Warning "Not every system had answered after 10 seconds; check the person's Password tab." } Propagates the password and stays on the line for up to ten seconds to be told which systems took it. .EXAMPLE $systems = (Get-JIMMetaverseObject -Id $id).ConnectedSystemObjects.ConnectedSystemId Set-JIMMetaverseObjectPassword -Id $id -ConnectedSystemId $systems -Generate -Force Sets one generated password on every account the person has, whatever each system's Password Synchronisation configuration says. .LINK Set-JIMConnectedSystemObjectPassword Get-JIMPendingPasswordChange Get-JIMMetaverseObject #> <# Four parameter sets, because two independent choices are made here and the binder enforces what goes with what: where the password is aimed (named systems, or every configured system) and where it comes from (supplied, or generated by JIM). -EnableAccount lives only in the Named sets so the binder, rather than a runtime check, refuses it for a propagated change. The default set is the propagate case with a supplied password (decision D5): the event case, which needs no account selection. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'PropagateSuppliedPassword')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [guid]$Id, [Parameter(Mandatory, ParameterSetName = 'NamedSuppliedPassword')] [Parameter(Mandatory, ParameterSetName = 'NamedGeneratedPassword')] [ValidateNotNullOrEmpty()] [int[]]$ConnectedSystemId, [Parameter(Mandatory, ParameterSetName = 'NamedSuppliedPassword')] [Parameter(Mandatory, ParameterSetName = 'PropagateSuppliedPassword')] [ValidateNotNull()] [securestring]$Password, [Parameter(Mandatory, ParameterSetName = 'NamedGeneratedPassword')] [Parameter(Mandatory, ParameterSetName = 'PropagateGeneratedPassword')] [switch]$Generate, [Parameter()] [ValidateSet('RequireChangeAtNextSignIn', 'ExpiresAccordingToTargetPolicy', 'NeverExpires')] [string]$ExpiryBehaviour, [Parameter(ParameterSetName = 'NamedSuppliedPassword')] [Parameter(ParameterSetName = 'NamedGeneratedPassword')] [switch]$EnableAccount, [Parameter()] [ValidateRange(0, 30)] [int]$Wait, [Parameter()] [switch]$Force ) process { if (-not $script:JIMConnection) { Write-Error "You are not connected to JIM. Run Connect-JIM -Url <your JIM URL> to authenticate, then try again." return } $namedAccounts = $PSBoundParameters.ContainsKey('ConnectedSystemId') # The person's accounts are needed to resolve named systems to the accounts the API takes, and to know # which systems a generated password has to satisfy. A propagated change with a supplied password needs # neither, and the server resolves its own targets. $accounts = @() if ($namedAccounts -or $Generate) { try { $metaverseObject = Invoke-JIMApi -Endpoint "/api/v1/metaverse/objects/$Id" -Method 'GET' } catch { Write-Error "Failed to read the Metaverse Object's accounts: $_" return } $accounts = @($metaverseObject.ConnectedSystemObjects) } $connectedSystemObjectIds = $null if ($namedAccounts) { $systemIds = @($ConnectedSystemId | Select-Object -Unique) $accounts = @($accounts | Where-Object { $systemIds -contains $_.ConnectedSystemId }) # Refused rather than quietly narrowed. A caller who named three systems and had the password set in # two would believe all three took it. $missing = @($systemIds | Where-Object { $_ -notin @($accounts | ForEach-Object { $_.ConnectedSystemId }) }) if ($missing.Count -gt 0) { Write-Error "This Metaverse Object has no account in Connected System $($missing -join ', '). Nothing was set." return } $connectedSystemObjectIds = @($accounts | ForEach-Object { $_.Id }) } if ($Generate) { # Generated against every system the password is going to, not one of them: one password has to # satisfy the strictest of several systems, and an administrator cannot see those policies to reason # about them. JIM reconciles them and refuses outright where no single password can satisfy them # all, which is far better than a password accepted by the first system and refused by the second. $policySystemIds = @($accounts | ForEach-Object { $_.ConnectedSystemId } | Select-Object -Unique) if ($policySystemIds.Count -eq 0) { Write-Error "This Metaverse Object has no accounts, so there is no Connected System policy to generate a password against. Supply one with -Password instead." return } try { $generated = Invoke-JIMApi -Endpoint "/api/v1/synchronisation/connected-systems/generate-password" -Method 'POST' -Body @{ connectedSystemIds = $policySystemIds } } catch { Write-Error "Failed to generate a password for these accounts: $_" return } $plainPassword = $generated.password $generatedPassword = ConvertTo-SecureString -String $plainPassword -AsPlainText -Force foreach ($systemName in $generated.systemsWithNoDiscoveredPolicy) { Write-Warning "JIM could read no password policy from $systemName, so it cannot promise this password will be accepted there." } } else { # A SecureString on the parameter so a password never sits in the session's command history in clear # text. It is unwrapped once here to be sent; the wire format is JSON over TLS. $plainPassword = ConvertFrom-SecureString -SecureString $Password -AsPlainText } if ([string]::IsNullOrWhiteSpace($plainPassword)) { Write-Error "A password is required." return } $body = @{ password = $plainPassword } if ($namedAccounts) { $body.connectedSystemObjectIds = $connectedSystemObjectIds } if ($PSBoundParameters.ContainsKey('ExpiryBehaviour')) { $body.expiryBehaviour = $ExpiryBehaviour } # Only sent when asked for. Omitting it means "leave the accounts' enabled state alone"; sending false # would ask each Connected System to disable an account nobody asked it to touch. if ($EnableAccount) { $body.enableAccount = $true } # Sent only when asked for. The server's defaults per mode are the contract for a request that names no # wait, and an explicit value would pin them into every script written against this version. if ($PSBoundParameters.ContainsKey('Wait')) { $body.wait = $Wait } if ($namedAccounts) { $target = "$($accounts.Count) account(s) of Metaverse Object $Id in Connected System $($systemIds -join ', ')" $action = "Set the password on these accounts" } else { $target = "Metaverse Object $Id" $action = "Set the password on every Connected System configured for Password Synchronisation" } if (-not ($Force -or $PSCmdlet.ShouldProcess($target, $action))) { return } Write-Verbose "Setting a password for Metaverse Object $Id$(if ($namedAccounts) { " on $($accounts.Count) named account(s)" } else { " on every configured Connected System" })" try { $result = Invoke-JIMApi -Endpoint "/api/v1/metaverse/objects/$Id/password" -Method 'POST' -Body $body } catch { Write-Error "Failed to set the password: $_" return } # A generated password is carried on the result: the caller never had it, and it is not recoverable once # this call returns. Withholding it would set a password nobody can use. if ($Generate) { $result | Add-Member -NotePropertyName 'GeneratedPassword' -NotePropertyValue $generatedPassword -Force } # The result first; then a refusal per target, in the system's own words, because a parked password is # something the caller has to act on. The result rides on each error's TargetObject, so a script that # stops on errors can still read the other targets from the exception it caught. $result foreach ($parkedTarget in @($result.Targets | Where-Object { $_.State -eq 'Parked' })) { Write-Error -Message "$($parkedTarget.ConnectedSystemName) refused the password: $($parkedTarget.Message)" -TargetObject $result } if ($null -ne $result.PSObject.Properties['Settled'] -and -not $result.Settled -and $PSBoundParameters.ContainsKey('Wait') -and $Wait -gt 0) { Write-Warning "Not every Connected System had answered within $Wait second(s). Delivery continues; follow Activity $($result.ActivityId) or the person's Password tab." } } } |