Skip to content

Commit 05faac1

Browse files
committed
fix: DSRM membership endpoint expects member_names, not role_names
Root cause: dsrmPath() emitted ?role_names=<r>, but FlashBlade API v2.22 (and v2.23) exposes POST/GET/DELETE /management-access-policies/directory-services/roles with query params policy_names + member_names. The role IS the member of the membership relation. Real-array apply returned: HTTP 400: Member identifier is required. GET masked the bug because the API silently ignores unknown query params and returns the full collection, which our wrapper happened to filter down by other means. Fix: - Client dsrmPath: role_names -> member_names (applies to Get/Post/Delete) - Mock handler: accept/require member_names on all 3 methods - Client test: assert member_names on POST recorded request Discovered during Phase 50.1 UAT against real arrays par5/pa7 on 2026-04-17. Phase 50.1 goal (DSR POST ?names=) was validated successful; this is a distinct defect in the sibling membership resource delivered by Phase 50.
1 parent d2efa6b commit 05faac1

3 files changed

Lines changed: 13 additions & 11 deletions

internal/client/management_access_policy_directory_service_role_memberships.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"net/url"
77
)
88

9-
// dsrmPath builds "/management-access-policies/directory-services/roles?policy_names=<p>&role_names=<r>".
9+
// dsrmPath builds "/management-access-policies/directory-services/roles?policy_names=<p>&member_names=<r>".
10+
// FlashBlade API v2.22 expects `member_names` (not `role_names`) on this endpoint — the role IS the
11+
// member of the membership relation. Using `role_names` yields HTTP 400 "Member identifier is required".
1012
func dsrmPath(policyName, roleName string) string {
1113
v := url.Values{}
1214
v.Set("policy_names", policyName)
13-
v.Set("role_names", roleName)
15+
v.Set("member_names", roleName)
1416
return "/management-access-policies/directory-services/roles?" + v.Encode()
1517
}
1618

internal/client/management_access_policy_directory_service_role_memberships_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func assertDSRMQueryParams(t *testing.T, r *http.Request) {
3636
if got := r.URL.Query().Get("policy_names"); got != "pure:policy/array_admin" {
3737
t.Errorf("expected policy_names=pure:policy/array_admin, got %q", got)
3838
}
39-
if got := r.URL.Query().Get("role_names"); got != "admin-role" {
40-
t.Errorf("expected role_names=admin-role, got %q", got)
39+
if got := r.URL.Query().Get("member_names"); got != "admin-role" {
40+
t.Errorf("expected member_names=admin-role, got %q", got)
4141
}
4242
}
4343

internal/testmock/handlers/management_access_policy_directory_service_role_memberships.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ func (s *mapDsrMembershipsStore) handle(w http.ResponseWriter, r *http.Request)
5151
}
5252

5353
// handleGet returns the pair when found, or empty list + 200 on miss.
54-
// Both policy_names and role_names are optional individually; when both are provided,
54+
// Both policy_names and member_names are optional individually; when both are provided,
5555
// the response is filtered to that exact pair.
5656
func (s *mapDsrMembershipsStore) handleGet(w http.ResponseWriter, r *http.Request) {
57-
if !ValidateQueryParams(w, r, []string{"policy_names", "role_names"}) {
57+
if !ValidateQueryParams(w, r, []string{"policy_names", "member_names"}) {
5858
return
5959
}
6060
pName := r.URL.Query().Get("policy_names")
61-
rName := r.URL.Query().Get("role_names")
61+
rName := r.URL.Query().Get("member_names")
6262

6363
s.mu.Lock()
6464
defer s.mu.Unlock()
@@ -79,14 +79,14 @@ func (s *mapDsrMembershipsStore) handleGet(w http.ResponseWriter, r *http.Reques
7979
// handlePost is idempotent: creates the pair if absent, returns 200 with the pair either way.
8080
// This resolves Q3 from 50-CONTEXT.md — Terraform replays never produce 409 conflicts.
8181
func (s *mapDsrMembershipsStore) handlePost(w http.ResponseWriter, r *http.Request) {
82-
if !ValidateQueryParams(w, r, []string{"policy_names", "role_names"}) {
82+
if !ValidateQueryParams(w, r, []string{"policy_names", "member_names"}) {
8383
return
8484
}
8585
pName, okP := RequireQueryParam(w, r, "policy_names")
8686
if !okP {
8787
return
8888
}
89-
rName, okR := RequireQueryParam(w, r, "role_names")
89+
rName, okR := RequireQueryParam(w, r, "member_names")
9090
if !okR {
9191
return
9292
}
@@ -105,14 +105,14 @@ func (s *mapDsrMembershipsStore) handlePost(w http.ResponseWriter, r *http.Reque
105105

106106
// handleDelete removes the pair from the set. Idempotent — missing pair is silently ignored.
107107
func (s *mapDsrMembershipsStore) handleDelete(w http.ResponseWriter, r *http.Request) {
108-
if !ValidateQueryParams(w, r, []string{"policy_names", "role_names"}) {
108+
if !ValidateQueryParams(w, r, []string{"policy_names", "member_names"}) {
109109
return
110110
}
111111
pName, okP := RequireQueryParam(w, r, "policy_names")
112112
if !okP {
113113
return
114114
}
115-
rName, okR := RequireQueryParam(w, r, "role_names")
115+
rName, okR := RequireQueryParam(w, r, "member_names")
116116
if !okR {
117117
return
118118
}

0 commit comments

Comments
 (0)