Repository navigation
Give New-PfbApiClient a typed body and require legal-hold released - #140
Merged
juemerson-at-purestorage merged 3 commits intoAug 21, 2026
Merged
juemerson-at-purestorage merged 3 commits into
juemerson-at-purestorage merged 3 commits into
Conversation
Both fixes are the confirmed findings of the issue dmann000#106 Part 2 audit: cmdlets that build a semantic empty `@{}` body and, in doing so, send a request the published contract forbids. Refs dmann000#106. New-PfbApiClient -- impossible through the public interface ----------------------------------------------------------- `#/components/schemas/ApiClientsPost` requires body property `public_key` on REST 2.0-2.28, and `max_role` on 2.0-2.18 (optional and deprecated from 2.19). The cmdlet exposed only `-Name`, `-Attributes` and `-Array`, so New-PfbApiClient -Name 'automation-client' bound cleanly and sent `{}`. There was no typed route to either required field; the only way to succeed was to hand-build `-Attributes`, which dmann000#106 treats as an escape hatch rather than the intended interface. Adds a `Typed` default parameter set with mandatory `-PublicKey` and optional `-MaxRole`, and moves `-Attributes` into its own mandatory set so a mixed invocation is rejected at bind time instead of letting the raw hashtable silently override a typed value. This is the shape New-PfbObjectStoreAccountExport already uses for the same problem (the dmann000#101 fix), down to the comment noting that a reference's `resource_type` is readOnly and must never be sent. `-MaxRole` builds `max_role = @{ name = ... }`; the Attributes branch now uses `.Clone()` so the caller's hashtable cannot be mutated. Deliberately NOT done: no runtime REST-version check making `-MaxRole` mandatory below 2.19. The defect being fixed is that no typed path existed, and version-conditional mandatory-ness is not a pattern this module has -- inventing one here would be a larger decision than this fix. The version boundary is documented in the help instead. The other optional body properties (`access_policies`, `access_token_ttl_in_ms`, `issuer`) stay with dmann000#57/dmann000#65. Update-PfbLegalHoldEntity -- typed but not enforced --------------------------------------------------- A category dmann000#106 did not anticipate, and the reason nothing had flagged it. In every spec from fb2.17.json through fb2.28.json, `PATCH /legal-holds/held-entities` references `#/components/parameters/Legal_holds_release`, resolving to `released`, `in: query`, `required: true`. `-Released` existed but was optional, and the key was sent only when explicitly bound, so Update-PfbLegalHoldEntity -Name 'fs1' -Recursive $true omitted it. Two of the cmdlet's own examples documented that shape. Because the drift report scores typed-parameter PRESENCE rather than requiredness, the endpoint read as fully covered -- filed separately as a tooling follow-up. `-Released` becomes mandatory `[bool]` and is always sent. A mandatory nullable boolean is a contradiction, and plain `[bool]` still binds `-Released $false`, which matters because false is the APPLY direction. This is the one parameter in the file not guarded by ContainsKey; the comment says why -- the convention exists to distinguish "not supplied" from "supplied as false", a distinction that cannot arise for a parameter with no legal omission. The help needed more than a parameter line. The description claimed the entity is identified by name and changed via Attributes, and neither is true: a held entity has no name at all (`LegalHoldHeldEntity` declares only `file_system`, `legal_hold`, `path`, `status`), and the endpoint accepts no request body. Two of three examples would no longer bind, so they were not merely stale but uncallable. All three are replaced with forms measured on a live array. Derived reports --------------- Reports/PfbDeadKeyReport.json and Reports/PfbApiDriftReport.json|.md are regenerated because both carry content-identity gates that the source change reds. Verified as caused-by-this-change rather than assumed: the failures were attributed by stashing the diff, confirming both gates pass on a clean tree, and unstashing. dead keys 83 -> 83 (the ceiling holds) parametersInventoried 2165 -> 2167 (the two new typed parameters) skipReason body property 278 -> 280 drift: missing addable body properties 426 -> 424 The `body property` skip reason is the one CommittedDeadKeyReport.Tests.ps1 deliberately leaves unceilinged, its comment predicting exactly this case -- that ceilinging it would red any PR adding a typed body parameter. The drift report drops `public_key` and `max_role` from the POST /api-clients gap row, and both lose their systemicGaps aggregate rows by falling from two endpoints to one. PATCH /api-clients keeps its own `max_role` gap row, so the Task 8 canary still fires. All three reports are written LF-only to match their committed form. Verification ------------ Scoped, dual-edition, per the run-pester-tests skill -- the aggregate suite is never a completion check here. changed cmdlets + siblings 60 passed / 0 failed, both editions report gates + module-wide tripwires (11) 537 pwsh 7 / 457 WinPS 5.1, 0 failed containers ok on every row, both editions Both cmdlets were driven test-first. New-PfbApiClient.Tests.ps1 is new -- the cmdlet had no test file -- and failed 4 of 7 against the unmodified source before passing 7 of 7 after. Update-PfbLegalHoldEntity.Tests.ps1 had every invocation updated to bind, and its "omits recursive and released entirely when not supplied" assertion split: the recursive half kept, the released half replaced by an always-present one. Both mandatory-ness tests assert on command METADATA rather than by invoking, because an unbound mandatory parameter prompts rather than throwing and hangs a non-interactive run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's "Verify derived artifacts are not stale" job found two more artifact pairs
stale beyond the drift and dead-key reports already in this branch. Both walk
cmdlet PARAMETER metadata, so any typed-parameter change moves them:
Reports/PfbFieldCmdletMap.json + PfbFieldCmdletMapping.md
Reports/PfbPipelineSelectorMap.json + PfbPipelineSelectorMap.md
Regenerated through scripts/Assert-PfbDerivedArtifacts.ps1 -KeepWorkDirectory
rather than by running the generators bare, per that script's own instruction:
Build-PfbFieldCmdletMap.ps1 records what it finds in the whole spec directory
(availableSpecVersions, processed-version counts), so a bare run against a cache
holding anything past the 29 pinned versions writes different output and the gate
reports the same artifact stale again. All four written LF-only to match their
committed form.
FieldCmdletMap: +20 lines, `no-spec-enum-found` 1972 -> 1974 -- the two new
New-PfbApiClient parameters, neither of which has a spec enum to match.
PipelineSelectorMap is the interesting one, and worth reading rather than
rubber-stamping. Making -Released mandatory moves two Update-PfbLegalHoldEntity
probe rows into `BindError`/`HarnessRefusal` ("would prompt for an unbound
mandatory parameter"), where they had been `Bound` and `Unbindable`:
BindError 2 -> 4
Bound 578 -> 577
Unbindable 234 -> 233
assistedRows 213 -> 212
Note what that costs. `BindError` is the one outcome that map's own legend calls
"triage -- the harness never invoked, the only unmeasured outcome", so this cmdlet
has gone from a MEASURED selector result to an unmeasured one. That is inherent to
the fix rather than a defect in it: the probe supplies a selector and nothing else,
and a required parameter it does not know to supply cannot be bound. The row it
replaces was `Bound` with `names=PROBE-name` -- a measurement of a selector that
dmann000#139 establishes cannot identify a held entity anyway, so little is actually lost.
The general shape is worth someone's attention though, because it will recur: any
future fix that correctly makes a required parameter mandatory will silently
convert its pipeline-selector coverage into a triage row. The probe harness would
need to learn to satisfy mandatory parameters outside the selector under test for
that coverage to survive. Not filed, because one cmdlet is not yet evidence of a
pattern -- but this is the first instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tests pin the pipeline-selector map's BindError row count at 2. Making Update-PfbLegalHoldEntity -Released mandatory takes it to 4, and both reds are correct behaviour from tripwires doing their job rather than defects in them: Tests/PfbPipelineSelectorRail.Tests.ps1 'BindError stays at N rows' Tests/Build-PfbPipelineSelectorMap.Tests.ps1 'records only BindError as unmeasured' BindError is the map's only UNMEASURED outcome -- Unbindable and CmdletError are verdicts carrying evidence. The pins exist because a rise means the probe harness has started refusing cmdlets it used to measure, which is the regression that produced the original 33-pair blind spot. So a rise is exactly what should stop a build, and re-baselining is only legitimate with the movement accounted for. Accounted for: both new rows are Update-PfbLegalHoldEntity/Name, refused with "would prompt for an unbound mandatory parameter -- __AllParameterSets: Released". They were Bound and Unbindable before. The probe supplies a selector and nothing else, so a REQUIRED parameter it does not know to supply cannot be bound. Little real coverage is lost: the row it replaces measured `names=PROBE-name` on a key that cannot identify a held entity at all (filed as dmann000#139). Exact counts now, since the old comment's arithmetic is part of what went stale -- ErrorKind non-null on 359 rows: 233 InputObjectNotBound, 122 CmdletError, 2 ParameterBindingError, 2 HarnessRefusal. Only the last two kinds classify as the BindError outcome; the previous comment said only ParameterBindingError did, which was true before HarnessRefusal appeared here. The comment on the Rail test also records the part that generalises, because this will recur: any fix that correctly makes a required parameter mandatory converts that cmdlet's pipeline-selector coverage into a triage row. If the number climbs again for that reason the answer is to teach the probe generator to satisfy mandatory parameters outside the selector under test, not to keep re-baselining. Worth noting which edition catches which, since it is not symmetric: the Rail test regenerates and is PS7-gated, while the Build- test reads the COMMITTED report and so reds on Windows PowerShell 5.1 as well. Neither appeared in this branch's earlier scoped runs, which is the module-wide-tripwire gap -- a test pinning a whole-repo total belongs to no changed file's touched set. CI found both. Verified after the change: 170 passed pwsh 7, 40 passed / 130 skipped WinPS 5.1, 0 failed, containers ok on both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the two confirmed findings of the issue #106 Part 2 audit: cmdlets that build a semantic empty
@{}body and, in doing so, send a request the published contract forbids.Closes #106 — Part 2 only. Part 1 of that issue (
New-PfbObjectStoreAccessPolicyRuleomitting the requirednamesquery parameter) was fixed separately and landed onmainin #134; this PR completes the Part 2 audit and fixes what it found, which is the last of #106's two deliverables. The audit record itself is a comment on the issue rather than duplicated here.New-PfbApiClient— impossible through the public interface#/components/schemas/ApiClientsPostrequires body propertypublic_keyon REST 2.0-2.28, andmax_roleon 2.0-2.18 (optional and deprecated from 2.19). The cmdlet exposed only-Name,-Attributesand-Array, sobound cleanly and sent
{}. There was no typed route to either required field; the only way to succeed was to hand-build-Attributes, which #106 treats as an escape hatch rather than the intended interface.Adds a
Typeddefault parameter set with mandatory-PublicKeyand optional-MaxRole, and moves-Attributesinto its own mandatory set so a mixed invocation is rejected at bind time instead of letting the raw hashtable silently override a typed value. This is the shapeNew-PfbObjectStoreAccountExportalready uses for the same problem (the #101 fix), down to the comment noting that a reference'sresource_typeis readOnly and must never be sent. The-Attributesbranch now uses.Clone()so the caller's hashtable cannot be mutated.Deliberately not done: no runtime REST-version check making
-MaxRolemandatory below 2.19. The defect being fixed is that no typed path existed, and version-conditional mandatory-ness is not a pattern this module has — inventing one here is a larger decision than this fix. The version boundary is documented in the help instead. The other optional body properties (access_policies,access_token_ttl_in_ms,issuer) stay with #57/#65.Update-PfbLegalHoldEntity— typed but not enforcedA category #106 did not anticipate, and the reason nothing had flagged it. In every spec from
fb2.17.jsonthroughfb2.28.json,PATCH /legal-holds/held-entitiesreferences#/components/parameters/Legal_holds_release, resolving toreleased,in: query,required: true.-Releasedexisted but was optional, and the key was sent only when explicitly bound, soomitted it. Two of the cmdlet's own examples documented that shape.
-Releasedbecomes mandatory[bool]and is always sent. A mandatory nullable boolean is a contradiction, and plain[bool]still binds-Released $false, which matters because false is the apply direction. This is the one parameter in the file not guarded byContainsKey; the comment says why — the convention exists to distinguish "not supplied" from "supplied as false", a distinction that cannot arise for a parameter with no legal omission.The help needed more than a parameter line. The description claimed the entity is identified by name and changed via
-Attributes, and neither is true: a held entity has no name at all (LegalHoldHeldEntitydeclares onlyfile_system,legal_hold,path,status) and the endpoint accepts no request body. Two of three examples would no longer bind, so they were not merely stale but uncallable. All three are replaced with forms measured on a live array.Because the drift report scores typed-parameter presence rather than requiredness, this endpoint read as fully covered — that blind spot is #137.
Derived reports
Four artifact pairs move, not two.
PfbDeadKeyReport.jsonandPfbApiDriftReport.json|.mdwere regenerated up front; CI's "Verify derived artifacts are not stale" job then foundPfbFieldCmdletMap.json|Mapping.mdandPfbPipelineSelectorMap.json|.mdstale as well, since both also walk cmdlet parameter metadata. Those two were regenerated throughscripts/Assert-PfbDerivedArtifacts.ps1 -KeepWorkDirectoryrather than by running the generators bare, per that script's own warning:Build-PfbFieldCmdletMap.ps1records what it finds in the whole spec directory, so a bare run against a cache holding anything past the 29 pinned versions writes different output and the gate reports the same artifact stale again.PfbFieldCmdletMapmoves byno-spec-enum-found1972 → 1974 — the two new parameters, neither of which has a spec enum to match.PfbPipelineSelectorMapis the one worth reading rather than rubber-stamping, and it cost two test re-baselines. Making-Releasedmandatory means the probe harness can no longer construct a call forUpdate-PfbLegalHoldEntity, so its two rows move fromBound/UnbindabletoBindError/HarnessRefusal("would prompt for an unbound mandatory parameter").BindErroris that map's only unmeasured outcome, and two tests pin it at 2 precisely so a rise stops the build — it is the regression shape that produced the original 33-pair blind spot.Both reds were the tripwires working. Re-baselined 2 → 4 with the movement accounted for in the comments rather than absorbed: the probe supplies a selector and nothing else, so a required parameter it does not know to supply cannot be bound, and the row it replaces measured
names=PROBE-nameon a key that cannot identify a held entity anyway (#139). The comments also record the part that generalises — any future fix correctly making a required parameter mandatory will convert that cmdlet's pipeline-selector coverage into a triage row, and if this number climbs again for that reason the answer is to teach the probe generator to satisfy mandatory parameters outside the selector under test, not to keep re-baselining.Neither pin appeared in this branch's scoped runs, which is the module-wide-tripwire gap: a test asserting a whole-repo total belongs to no changed file's touched set. Note they are not symmetric either — the
PfbPipelineSelectorRailpin regenerates and is PS7-gated, while theBuild-PfbPipelineSelectorMapone reads the committed report and reds on 5.1 too. CI caught both.The two originally-regenerated reports carry content-identity gates that the source change also reds. Note that the
Committed*Report.Tests.ps1pair is not the gate — those are shape and absolute-path guards that pass regardless; the real gates live inBuild-PfbDeadKeyReport.Tests.ps1andBuild-PfbApiDriftReport.Tests.ps1.Verified as caused-by-this-change rather than assumed: each failure was attributed by stashing the diff, confirming the gate passes on a clean tree, then unstashing.
parametersInventoriedbody propertyThe dead-key total is a ceiling and holds. The
body propertyskip reason is the oneCommittedDeadKeyReport.Tests.ps1deliberately leaves unceilinged, its comment predicting exactly this case — that ceilinging it would red any PR adding a typed body parameter.The drift report drops
public_keyandmax_rolefrom thePOST /api-clientsgap row, and both lose theirsystemicGapsaggregate rows by falling from two endpoints to one.PATCH /api-clientskeeps its ownmax_rolegap row, so the Task 8 canary still fires. All three reports are written LF-only to match their committed form; the generators emit CRLF on Windows, whichautocrlfhides fromgit diffwhile leaving a mixed-endings file.Verification
Scoped and dual-edition throughout — the aggregate suite is CI's job, never a task's completion check here.
Container okon every row, both editions.Rebased onto
mainafter #135 landed, which replaced the globalMaxSkippedceiling with the per-fileExpectedSkipsmap. No baseline entry is needed for the new test file:Assert-PfbTestCoverage.ps1fails an undeclared file that skips anything at all, and both changed test files skip zero on both editions (17 passed / 0 skipped each), so there is nothing to declare. Confirmed by running them alone rather than inferring it from a mixed run —CiCoverageGate.Tests.ps1exercises the gate against synthetic results, so its own green says nothing about this branch's skip profile.Both cmdlets were driven test-first.
Tests/New-PfbApiClient.Tests.ps1is new — the cmdlet had no test file — and failed 4 of 7 against the unmodified source before passing 7 of 7.Tests/Update-PfbLegalHoldEntity.Tests.ps1had every invocation updated to bind, and its "omits recursive and released entirely when not supplied" assertion split: therecursivehalf kept, thereleasedhalf replaced by an always-present one. Both mandatory-ness tests assert on command metadata rather than by invoking, because an unbound mandatory parameter prompts rather than throwing and hangs a non-interactive run.Live verification
This PR touches
Public/, so the live-FlashBlade requirement applies in full. No exemption is claimed.Update-PfbLegalHoldEntitywas verified on a physical FlashBlade (Purity//FB 4.8.2, REST 2.26) against this branch's committed code — 7 calls, 7 Pass:The teardown is the proof rather than a formality: while a hold is applied the array refuses to delete it (
Can't delete legal hold because it is still in use.) and refuses to destroy the file system (File system with legal holds applied does not support destroy.). Those two steps succeeding is what shows the release actually landed. The array was left clean — zero holds, zero held entities, zero test file systems.The working release form was measured rather than assumed, and is what the new help examples show:
Two things could not be verified live, stated rather than glossed:
New-PfbApiClientcannot be live-tested at all.api-clientsis a denied family in the live-test harness — minting real credentials on the lab array is out of scope for it — so the call recordsSkipped-Policywith the family resolved from the cmdlet's own endpoint rather than from a caller-supplied string. That is the rail working, not a coverage gap that was routed around. Its evidence is the dual-edition unit coverage above.-Releasedis now an unbound mandatory parameter, which prompts rather than throws and would hang a non-interactive run, so there is no request left to send. Mandatory-ness is asserted from command metadata instead. Worth recording that a harness dry run returnedWouldRunfor that shape: it validates supplied arguments against the module's real parameters but does not check mandatory satisfaction, so a dry run could never have proven this either way.Not in this PR
CHANGELOG.mdedit — those are maintainer decisions.-NameonUpdate-PfbLegalHoldEntitystays mandatory even though it is an unusable selector. Held entities have no name, sonamescan never match one; a live array rejects a request selected only by it withEither names or ids query parameter is requireddespite the key being sent. Demoting a mandatory parameter is a breaking change for positional callers, so it is filed as Update-PfbLegalHoldEntity -Name is a mandatory but unusable selector: a held entity has no name, and the array rejects a request selected only by it #139 rather than folded in here.Follow-ups filed from the audit
releaseddefect was invisible to it.New-PfbNlmReclamation -Nameis a mandatory dead selector: the endpoint declares nonamesparameter and the operation is array-wide.Update-PfbLegalHoldEntity -Nameis a mandatory but unusable selector.One correction to #106's own arithmetic
The list of 23 was built by matching one exact literal,
$body = if ($Attributes) { $Attributes } else { @{} }. Three cmdlets use a.Clone()variant the pattern does not match (New-PfbLocalDirectoryService,New-PfbLocalGroup,New-PfbFileSystemExport), so the convention is 25 cmdlets, not 23. All three were audited.New-PfbLocalGroupis confirmed broken on the wire for this same failure mode and is #136; the other two came back clean.The convention has now produced four broken creates (#101, #136, and the two fixed here). For the next sweep of it, match on
else { @{} }or on the AST rather than on the whole assignment —.Clone()is a deliberate improvement that avoids mutating the caller's hashtable, so more such variants are likely, and a literal-string sweep under-reports while reading exactly like a complete one.