mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 09:30:46 +02:00
* 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. * feat(iam): account-scoped OIDC providers Add OIDCProviderRecord.AccountID enforcement: when a role lives in account A, the OIDC provider validating the assume-role token must be either global (AccountID="") or also live in account A. Cross-account use is rejected at the IAM-manager layer before reaching the trust policy validator. OIDCProviderStore gains GetProviderByIssuerAndAccount; both the in- memory and filer-backed stores implement it. Static-config-only deployments are unaffected since they don't populate the store. * fix(iam): account-scoped lookup for cross-account check enforceProviderAccountScope was calling GetProviderByIssuer, which returns the first match arbitrarily when multiple providers share an issuer (one global + one per tenant is the canonical setup). On a two-record collision the wrong record could come back first and falsely reject a valid same-account or global-provider request. Use GetProviderByIssuerAndAccount as the primary lookup so the filter happens in the store. On miss, fall back to GetProviderByIssuer purely to distinguish "issuer entirely unknown" (let the STS layer reject) from "issuer registered in a different account" (surface a precise cross-account error). Addresses gemini high-priority review on PR #9323.
73 lines
2.0 KiB
Go
73 lines
2.0 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetProviderByIssuerAndAccount(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := NewMemoryOIDCProviderStore()
|
|
|
|
// Two providers for the same issuer, scoped to different accounts.
|
|
must := func(rec *OIDCProviderRecord) {
|
|
if err := store.StoreProvider(ctx, "", rec); err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
}
|
|
must(&OIDCProviderRecord{
|
|
AccountID: "111",
|
|
ARN: "arn:aws:iam::111:oidc-provider/idp.example",
|
|
URL: "https://idp.example",
|
|
ClientIDs: []string{"a"},
|
|
})
|
|
must(&OIDCProviderRecord{
|
|
AccountID: "222",
|
|
ARN: "arn:aws:iam::222:oidc-provider/idp.example",
|
|
URL: "https://idp.example",
|
|
ClientIDs: []string{"b"},
|
|
})
|
|
|
|
t.Run("matching account returns scoped record", func(t *testing.T) {
|
|
rec, err := store.GetProviderByIssuerAndAccount(ctx, "", "https://idp.example", "111")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if rec.AccountID != "111" {
|
|
t.Fatalf("unexpected record: %+v", rec)
|
|
}
|
|
})
|
|
|
|
t.Run("unknown account is rejected", func(t *testing.T) {
|
|
if _, err := store.GetProviderByIssuerAndAccount(ctx, "", "https://idp.example", "999"); err == nil {
|
|
t.Fatal("expected error for cross-account use")
|
|
}
|
|
})
|
|
|
|
t.Run("global provider satisfies any account", func(t *testing.T) {
|
|
must(&OIDCProviderRecord{
|
|
AccountID: "",
|
|
ARN: "arn:aws:iam:::oidc-provider/global.example",
|
|
URL: "https://global.example",
|
|
ClientIDs: []string{"x"},
|
|
})
|
|
rec, err := store.GetProviderByIssuerAndAccount(ctx, "", "https://global.example", "anything")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if rec.AccountID != "" {
|
|
t.Fatalf("expected global record, got AccountID=%q", rec.AccountID)
|
|
}
|
|
})
|
|
|
|
t.Run("empty caller account accepts any record", func(t *testing.T) {
|
|
rec, err := store.GetProviderByIssuerAndAccount(ctx, "", "https://idp.example", "")
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if rec == nil {
|
|
t.Fatal("expected a record")
|
|
}
|
|
})
|
|
}
|