Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
713d330
fix: correct bucket policy selector keys (#90)
juemerson-at-purestorage Aug 17, 2026
9d08ecc
fix: remove dead bucket policy selectors and fix audit-filter POST (#90)
juemerson-at-purestorage Aug 17, 2026
5a85a6e
fix: use resource-specific selector keys (#90)
juemerson-at-purestorage Aug 17, 2026
4717035
fix: preserve parent names for policy pipelines (#90)
juemerson-at-purestorage Aug 17, 2026
3fcf46f
fix: bind group and connection pipeline selectors (#90)
juemerson-at-purestorage Aug 17, 2026
8816146
fix: rename Get-PfbLocalGroupMember -Group to -GroupName (#90)
juemerson-at-purestorage Aug 17, 2026
4b82842
fix: remove unusable dead selector parameters (#90)
juemerson-at-purestorage Aug 17, 2026
cdb0af2
fix: restore pipeline binding on the surviving -Id selector (#90)
juemerson-at-purestorage Aug 17, 2026
9854b29
fix: correct remaining bucket selector keys (#90)
juemerson-at-purestorage Aug 17, 2026
35eb817
chore: correct misleading examples and tighten pipeline metadata asse…
juemerson-at-purestorage Aug 17, 2026
0d65043
fix: send the required names key on New-PfbBucketCorsPolicyRule (#90)
juemerson-at-purestorage Aug 17, 2026
9b2cbd0
@
juemerson-at-purestorage Aug 17, 2026
1f9ec52
fix: stop the bucket policy POST cmdlets binding a whole piped object…
juemerson-at-purestorage Aug 17, 2026
8efb810
fix: reject a stringified object bound into a selector parameter (#90)
juemerson-at-purestorage Aug 17, 2026
e0d573a
docs: correct the remaining four-pass and guard-attribution comments …
juemerson-at-purestorage Aug 17, 2026
abbacc2
chore: regenerate selector and dead-key reports after the #90 guards
juemerson-at-purestorage Aug 17, 2026
4da4fde
docs: record why some selector pairs cannot pipeline correctly (#90)
juemerson-at-purestorage Aug 17, 2026
064ecd0
docs: record the cross-endpoint ceiling on the remaining guarded cmdl…
juemerson-at-purestorage Aug 17, 2026
f7a5a74
fix: stop a documentation comment altering the generated selector map…
juemerson-at-purestorage Aug 17, 2026
b7ddff9
fix: make an array connection pipe correctly into Get-PfbArrayConnect…
juemerson-at-purestorage Aug 17, 2026
c9d377e
Remove -Id from Get-PfbArrayConnectionKey; the guard is the whole fix
juemerson-at-purestorage Aug 17, 2026
857775d
fix: detect whole-object selector coercion structurally
juemerson-at-purestorage Aug 18, 2026
f093159
fix: align selector surface and review documentation
juemerson-at-purestorage Aug 18, 2026
4fbc204
test: refresh dead-key baseline narrative
juemerson-at-purestorage Aug 18, 2026
d811492
docs: avoid promising an incomplete open-file request
juemerson-at-purestorage Aug 18, 2026
a45ea8a
docs: distinguish dead-key count from allowlist size
juemerson-at-purestorage Aug 18, 2026
bffcc57
test: remove obsolete dead-key allowlist ceilings
juemerson-at-purestorage Aug 18, 2026
9ed2e8b
docs: restore Get-PfbOpenFile examples
juemerson-at-purestorage Aug 18, 2026
4c34964
docs: correct the guard's own account of what it catches
juemerson-at-purestorage Aug 18, 2026
a2d4a55
fix: re-baseline the two selector population tripwires the fixes moved
juemerson-at-purestorage Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions Private/Assert-PfbSelectorNotCoerced.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
function Assert-PfbSelectorNotCoerced {
<#
.SYNOPSIS
Reject a selector value that is the ToString() of a whole piped object.
.DESCRIPTION
PowerShell resolves pipeline binding in FOUR passes: ByValue-without-coercion,
ByPropertyName-without-coercion, ByValue-WITH-coercion, then
ByPropertyName-WITH-coercion. A parameter that declares ValueFromPipeline is
therefore reachable at pass 3 by ANY object, whatever its shape.

Eighteen read cmdlets declare ValueFromPipeline on a name selector so that a
bare string can be piped ('policy1' | Get-PfbNfsExportRule). When the piped item
is an OBJECT that matches none of the cmdlet's parameters by property name,
pass 2 misses and pass 3 fires, ToString()-ing the entire object into the
[string[]] selector. The result is a garbage filter such as

policy_names=@{name=nfs-01; enabled=True; rules=}

which the array answers with HTTP 200 and the UNFILTERED collection, so the
caller believes they are looking at one policy's rules when they are looking at
every policy's rules. That silent mis-filtering is the issue-90 defect.

Several of these cmdlets Add-Member a lifted top-level property (PolicyName,
RoleName, GroupName) onto the items they return, which makes the self-chain
(Get-PfbNfsExportRule | Get-PfbNfsExportRule) bind at pass 2 and never reach this
guard. The lift cannot help the CROSS-endpoint chain
(Get-PfbNfsExportPolicy | Get-PfbNfsExportRule), because a policy item carries
`name`, not `PolicyName`. That residual is what this guard closes.

Note also that removing ValueFromPipeline does not make coercion impossible: a
ValueFromPipelineByPropertyName-only parameter whose ALIAS matches an
object-valued property still binds that object stringified at pass 4. This guard
covers that case too, since it inspects the bound value rather than the pass that
produced it.

Deliberately called imperatively from each cmdlet's process block rather than
wired as [ValidateScript({ ... })]. That was tried and reverted under issue #64:
a ValidateScript failure on a PIPELINE-bound argument is a NON-terminating
per-item binding error, so the cmdlet's end block still runs and still issues the
request -- with no selector key at all, i.e. the silently-unfiltered result this
guard exists to eliminate. The imperative throw terminates the pipeline, which is
the required behaviour.

Returns NOTHING on success, matching Assert-PfbAdminNameNotCoerced. Do not add a
`return $true`: callers invoke this bare, so a return value leaks into each
cmdlet's success stream.
.PARAMETER Value
The bound selector value. May be $null, a scalar, or a collection; every element
is checked. $null and an empty collection are accepted silently -- a parameter
that was never bound is not a defect.
.PARAMETER OriginalInput
The original process-block pipeline item. When it is a non-string object whose
ToString() result exactly equals the bound selector, and it exposes no property that
could bind this parameter, the whole item was coerced at pass 3. A directly supplied
selector has no process input and a piped resource name is already a string, so neither
is rejected. Callers must pass $PSItem from process; this is structural evidence and
avoids guessing from type-looking resource names.
.PARAMETER ParameterName
The parameter name WITHOUT its leading dash, e.g. 'PolicyName'. The dash is added
when the message is built.
.PARAMETER BindingPropertyName
Additional property names, normally parameter aliases, that can bind the selector.
Their presence suppresses the whole-input comparison because false rejection is worse
than a missed ambiguous case.

KNOWN GAP, stated rather than papered over: the '@{' signal catches an object-valued
property only when that value's ToString() contains '@{', which is true of a
PSCustomObject and false of a Hashtable or a generic Dictionary. So piping
[pscustomobject]@{ Group = @{ name = 'g1' } } binds at pass 4 via the Group alias,
yields the bare type name as the selector, and passes both signals -- the whole-input
comparison cannot help either, because the item's own ToString() is not the bound
value. Closing it means comparing against each binding property's ToString() too,
which widens the false-positive surface and is a maintainer's call rather than a
drive-by change. Reachability is low in practice: ConvertFrom-Json produces
PSCustomObject values, so a response object piped from a real cmdlet stays covered.
.PARAMETER Hint
Caller-facing, cmdlet-specific advice naming the property to pipe instead. Each
call site writes its own against what that family's producer actually returns.
#>
param(
[Parameter(Mandatory)] [AllowNull()] [AllowEmptyCollection()] [object]$Value,
[Parameter()] [AllowNull()] [object]$OriginalInput,
[Parameter(Mandatory)] [string]$ParameterName,
[Parameter()] [string[]]$BindingPropertyName = @(),
[Parameter(Mandatory)] [string]$Hint
)

if ($null -eq $Value) { return }

$inputPropertyNames = @($ParameterName) + @($BindingPropertyName)
$inputHasBindingProperty = $false
if ($null -ne $OriginalInput -and $OriginalInput -isnot [string]) {
foreach ($propertyName in $inputPropertyNames) {
if ($OriginalInput.PSObject.Properties[$propertyName]) {
$inputHasBindingProperty = $true
break
}
}
}

foreach ($v in @($Value)) {
# The original process item proves pass-3 whole-object coercion without interpreting
# resource-name text. Hashtable and generic-dictionary ToString() values are type names,
# so the previous '@{' signal missed them; prefix/type-resolution patterns would instead
# reject legitimate names such as 'System.backup'. Exact equality to this non-string
# input's own ToString() has no such naming heuristic. If a matching property exists, the
# binder may have selected it at pass 2 or pass 4, so do not infer whole-item coercion.
$wholeInputWasStringified = $v -is [string] -and
$null -ne $OriginalInput -and
$OriginalInput -isnot [string] -and
-not $inputHasBindingProperty -and
$v -ceq [string]$OriginalInput

# Keep the established PSCustomObject/pass-4 signal. .Contains() rather than -like: a
# literal test needs no wildcard semantics and cannot be broken by caller text.
if (($v -is [string] -and $v.Contains('@{')) -or $wholeInputWasStringified) {
# Built in one -f call: the format operator binds tighter than '+', so
# splitting this across concatenated literals silently formats only the last.
$message = '-{0} received a stringified object (''{1}'') instead of a name. ' +
'A piped object that matches none of this cmdlet''s parameters by property ' +
'name is coerced whole into -{0}, which sends a garbage filter and returns ' +
'the UNFILTERED collection. {2}'
throw ($message -f $ParameterName, $v, $Hint)
}
}
}
75 changes: 35 additions & 40 deletions Public/Bucket/Get-PfbBucketAccessPolicy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@ function Get-PfbBucketAccessPolicy {
Returns one or more bucket access policies from the FlashBlade array.
Bucket access policies define S3 bucket-level access controls. Filter
results by fully-qualified name (bucket/account:policy), or by bucket
member name/ID and policy name/ID separately.
name or bucket ID.

NOTE: The FlashBlade API requires at least one of -Name, -Id,
-MemberName, or -PolicyName to be specified.
GET /buckets/bucket-access-policies declares no policy-level selector;
'policy_names' exists only on the /rules variant, so use
Get-PfbBucketAccessPolicyRule -PolicyName for
that. It also declares no 'ids' selector, so there is no -Id parameter:
the endpoint silently ignored the key and returned the unfiltered
collection.
.PARAMETER Name
One or more fully-qualified bucket access policy names
(e.g. 'mybucket/myaccount:mypolicy').
.PARAMETER Id
One or more bucket access policy IDs.
.PARAMETER MemberName
.PARAMETER BucketName
One or more bucket names to retrieve access policies for.
.PARAMETER MemberId
.PARAMETER BucketId
One or more bucket IDs to retrieve access policies for.
.PARAMETER PolicyName
One or more access policy names to retrieve.
.PARAMETER PolicyId
One or more access policy IDs to retrieve.
.PARAMETER Filter
A server-side filter expression to narrow results.
.PARAMETER Sort
Expand All @@ -32,37 +30,35 @@ function Get-PfbBucketAccessPolicy {
.PARAMETER Array
The FlashBlade connection object. If not specified, the default connection is used.
.EXAMPLE
Get-PfbBucketAccessPolicy -MemberName "mybucket"
Get-PfbBucketAccessPolicy -BucketName "mybucket"

Returns access policies for the bucket named 'mybucket'.
.EXAMPLE
Get-PfbBucketAccessPolicy -Name "mybucket/myaccount:mypolicy"

Returns the specific bucket access policy by fully-qualified name.
.EXAMPLE
Get-PfbBucketAccessPolicy -PolicyName "read-only-policy" -MemberName "mybucket"
Get-PfbBucketAccessPolicy -BucketId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Returns a specific access policy for a specific bucket.
Returns access policies for the bucket with the given ID.
#>
[CmdletBinding(DefaultParameterSetName = 'ByMemberName')]
[CmdletBinding(DefaultParameterSetName = 'ByBucketName')]
param(
[Parameter(ParameterSetName = 'ByName')]
[string[]]$Name,

[Parameter(ParameterSetName = 'ById')]
[string[]]$Id,
# Within this family, the cross-endpoint chain from Get-PfbBucket into Get-PfbBucketAccessPolicy cannot
# filter correctly: a producer's bare `name` bound to -BucketName / `bucket_names` is the defect
# because `name` means different things by endpoint and metadata cannot identify its producer,
# so no correct generic binding exists. An undeclared or non-matching query key returns HTTP 200
# with the unfiltered collection; the guard's loud failure is therefore best. Do NOT remove it
# or add an alias: that flips WrongScalar to Bound while sending the wrong name; revisit only if
# the consumer can establish its producer, which parameter metadata alone cannot. Issue #90.
[Parameter(ParameterSetName = 'ByBucketName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]]$BucketName,

[Parameter(ParameterSetName = 'ByMemberName', ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]]$MemberName,

[Parameter(ParameterSetName = 'ByMemberId')]
[string[]]$MemberId,

[Parameter()]
[string[]]$PolicyName,

[Parameter()]
[string[]]$PolicyId,
[Parameter(ParameterSetName = 'ByBucketId')]
[string[]]$BucketId,

[Parameter()] [string]$Filter,
[Parameter()] [string]$Sort,
Expand All @@ -73,32 +69,31 @@ function Get-PfbBucketAccessPolicy {
begin {
Assert-PfbConnection -Array ([ref]$Array)
$allNames = [System.Collections.Generic.List[string]]::new()
$allIds = [System.Collections.Generic.List[string]]::new()
$allMemberNames = [System.Collections.Generic.List[string]]::new()
$allMemberIds = [System.Collections.Generic.List[string]]::new()
$allBucketNames = [System.Collections.Generic.List[string]]::new()
$allBucketIds = [System.Collections.Generic.List[string]]::new()
}

process {
Assert-PfbSelectorNotCoerced -Value $BucketName -OriginalInput $PSItem -ParameterName 'BucketName' -Hint (
'Pipe the bucket name instead, e.g. Get-PfbBucket | Select-Object -ExpandProperty name | ' +
'Get-PfbBucketAccessPolicy, or pass -BucketName explicitly.')
if ($Name) { foreach ($n in $Name) { $allNames.Add($n) } }
if ($Id) { foreach ($i in $Id) { $allIds.Add($i) } }
if ($MemberName) { foreach ($n in $MemberName) { $allMemberNames.Add($n) } }
if ($MemberId) { foreach ($i in $MemberId) { $allMemberIds.Add($i) } }
if ($BucketName) { foreach ($b in $BucketName) { $allBucketNames.Add($b) } }
if ($BucketId) { foreach ($i in $BucketId) { $allBucketIds.Add($i) } }
}

end {
$queryParams = @{}
Add-PfbCommonQueryParams -Into $queryParams -BoundParameters $PSBoundParameters -Names $allNames -Ids $allIds
if ($allMemberNames.Count -gt 0) { $queryParams['member_names'] = $allMemberNames -join ',' }
if ($allMemberIds.Count -gt 0) { $queryParams['member_ids'] = $allMemberIds -join ',' }
if ($PolicyName) { $queryParams['policy_names'] = $PolicyName -join ',' }
if ($PolicyId) { $queryParams['policy_ids'] = $PolicyId -join ',' }
Add-PfbCommonQueryParams -Into $queryParams -BoundParameters $PSBoundParameters -Names $allNames
if ($allBucketNames.Count -gt 0) { $queryParams['bucket_names'] = $allBucketNames -join ',' }
if ($allBucketIds.Count -gt 0) { $queryParams['bucket_ids'] = $allBucketIds -join ',' }

try {
Invoke-PfbApiRequest -Array $Array -Method GET -Endpoint 'buckets/bucket-access-policies' -QueryParams $queryParams -AutoPaginate
}
catch {
if ($_ -match 'Either names or ids' -or $_ -match 'Policy must be specified') {
Write-Warning "Bucket access policies require the -Name parameter with a fully-qualified 'bucket/policy' name, or the -Id parameter. Use Get-PfbObjectStoreAccessPolicy to list available policies."
Write-Warning "Bucket access policies require the -Name parameter with a fully-qualified 'bucket/policy' name, or -BucketName/-BucketId. Use Get-PfbObjectStoreAccessPolicy to list available policies."
return
}
throw
Expand Down
Loading
Loading