Files
seaweedfs/weed/iam/oidc/policy_claim_test.go
T
Chris Lu 1d3454ca5c feat(iam): claim-based policy mode for AssumeRoleWithWebIdentity (Phase 3b) (#9322)
* feat(iam): claim-based policy mode for AssumeRoleWithWebIdentity

When the caller passes the sentinel RoleArn arn:aws:iam:::role/sts-claim-based
(or omits it entirely) and the matched OIDC provider has policyClaim set,
mint a session whose effective policies come from that JWT claim instead
of from a server-side role mapping. Accepts string, comma-separated
string, or array shapes — MinIO-compatible behaviour for IDPs that
already attach policies to the user.

Trust-policy validation is skipped in claim-mode: the IDP is the sole
authority for both authentication and authorization, mirroring the
contract MinIO documents for its DummyRoleARN flow. Concrete-role mode
is unchanged and still requires the role definition + trust policy.

* fix(iam): trim policy-claim array elements + clean up stale comments

Three medium-priority cleanups gemini flagged on the claim-based path:

- extractClaimPolicies's array branch was leaving whitespace on each
  element while the string/comma-separated branch trimmed via
  splitPolicyClaimString. An IDP that emits ["readonly", " billing "]
  would create a "billing" policy lookup that didn't match the stored
  name. Trim every array element, drop empties.
- The "synthetic ARN keyed on the session name" comment was wrong —
  effectiveRoleArn here is the literal sentinel; it's the assumed-role
  ARN generated downstream that's session-keyed. Reword.
- The empty if/else block at the start of
  validateAssumeRoleWithWebIdentityRequest existed only to host a
  comment about deferred validation; the comment now lives in the
  function godoc and the empty branch is gone.

Addresses three gemini medium reviews on PR #9322.
2026-05-05 12:21:55 -07:00

58 lines
1.7 KiB
Go

package oidc
import (
"reflect"
"testing"
)
func TestExtractClaimPoliciesString(t *testing.T) {
got := extractClaimPolicies(map[string]interface{}{"policy": "readonly"}, "policy")
if !reflect.DeepEqual(got, []string{"readonly"}) {
t.Fatalf("got=%v", got)
}
}
func TestExtractClaimPoliciesCommaSeparated(t *testing.T) {
got := extractClaimPolicies(map[string]interface{}{"policy": "readonly, billing , "}, "policy")
if !reflect.DeepEqual(got, []string{"readonly", "billing"}) {
t.Fatalf("got=%v", got)
}
}
func TestExtractClaimPoliciesArray(t *testing.T) {
got := extractClaimPolicies(map[string]interface{}{
"policy": []interface{}{"readonly", "billing", 42, ""}, // non-string + empty filtered
}, "policy")
if !reflect.DeepEqual(got, []string{"readonly", "billing"}) {
t.Fatalf("got=%v", got)
}
}
func TestExtractClaimPoliciesMissing(t *testing.T) {
if got := extractClaimPolicies(map[string]interface{}{}, "policy"); got != nil {
t.Fatalf("expected nil, got %v", got)
}
}
func TestExtractClaimPoliciesEmptyClaimName(t *testing.T) {
// Provider not in claim-mode -> never read the claim, even if present.
if got := extractClaimPolicies(map[string]interface{}{"policy": "readonly"}, ""); got != nil {
t.Fatalf("empty claim name should return nil, got %v", got)
}
}
func TestExtractClaimPoliciesUnsupportedShape(t *testing.T) {
// Numeric / object / bool values should be ignored, not panic.
cases := []interface{}{
42,
3.14,
map[string]interface{}{"x": "y"},
true,
}
for _, v := range cases {
if got := extractClaimPolicies(map[string]interface{}{"policy": v}, "policy"); got != nil {
t.Fatalf("value %v: expected nil, got %v", v, got)
}
}
}