mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
A raw OIDC token sent as Authorization: Bearer was validated and mapped to a role through the provider's roleMapping, then authorized against the role's attached policies without ever consulting the role's trust policy. A federated user could act as a role that AssumeRoleWithWebIdentity would refuse to issue a session for with the same token. Run the same trust-policy validation before returning the principal on the bearer path.
151 lines
5.1 KiB
Go
151 lines
5.1 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// A raw OIDC token sent as Authorization: Bearer must clear the same role
|
|
// trust-policy gate as AssumeRoleWithWebIdentity. The provider's role mapping
|
|
// alone must not mint a principal for a role whose trust policy rejects the
|
|
// token's federated provider.
|
|
func TestOIDCBearerEnforcesRoleTrustPolicy(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
jwks := map[string]interface{}{
|
|
"keys": []map[string]interface{}{{
|
|
"kty": "RSA",
|
|
"kid": "bearer-test-key",
|
|
"use": "sig",
|
|
"alg": "RS256",
|
|
"n": base64.RawURLEncoding.EncodeToString(key.PublicKey.N.Bytes()),
|
|
"e": "AQAB",
|
|
}},
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/jwks" {
|
|
json.NewEncoder(w).Encode(jwks)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
provider := oidc.NewOIDCProvider("test-oidc")
|
|
require.NoError(t, provider.Initialize(&oidc.OIDCConfig{
|
|
Issuer: server.URL,
|
|
ClientID: "test-client",
|
|
JWKSUri: server.URL + "/jwks",
|
|
RoleMapping: &providers.RoleMapping{
|
|
Rules: []providers.MappingRule{
|
|
{Claim: "groups", Value: "denied-group", Role: "arn:aws:iam::role/BearerDeniedRole"},
|
|
{Claim: "groups", Value: "trusted-group", Role: "arn:aws:iam::role/BearerTrustedRole"},
|
|
},
|
|
},
|
|
}))
|
|
|
|
iamManager := integration.NewIAMManager()
|
|
require.NoError(t, iamManager.Initialize(&integration.IAMConfig{
|
|
STS: &sts.STSConfig{
|
|
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
|
|
MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
|
|
Issuer: "test-sts",
|
|
SigningKey: []byte("test-signing-key-32-characters-long"),
|
|
},
|
|
Policy: &policy.PolicyEngineConfig{DefaultEffect: "Deny", StoreType: "memory"},
|
|
Roles: &integration.RoleStoreConfig{StoreType: "memory"},
|
|
}, func() string { return "localhost:8888" }))
|
|
require.NoError(t, iamManager.RegisterIdentityProvider(provider))
|
|
|
|
require.NoError(t, iamManager.CreatePolicy(ctx, "", "BearerTestPolicy", &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{{
|
|
Effect: "Allow",
|
|
Action: []string{"s3:*"},
|
|
Resource: []string{"arn:aws:s3:::*", "arn:aws:s3:::*/*"},
|
|
}},
|
|
}))
|
|
|
|
trustPolicy := func(federatedProvider string) *policy.PolicyDocument {
|
|
return &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{"Federated": federatedProvider},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
}},
|
|
}
|
|
}
|
|
// trusts a different federated provider than the one issuing the token
|
|
require.NoError(t, iamManager.CreateRole(ctx, "", "BearerDeniedRole", &integration.RoleDefinition{
|
|
RoleName: "BearerDeniedRole",
|
|
TrustPolicy: trustPolicy("other-provider"),
|
|
AttachedPolicies: []string{"BearerTestPolicy"},
|
|
}))
|
|
require.NoError(t, iamManager.CreateRole(ctx, "", "BearerTrustedRole", &integration.RoleDefinition{
|
|
RoleName: "BearerTrustedRole",
|
|
TrustPolicy: trustPolicy("test-oidc"),
|
|
AttachedPolicies: []string{"BearerTestPolicy"},
|
|
}))
|
|
|
|
signJWT := func(groups ...string) string {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
|
"iss": server.URL,
|
|
"sub": "bearer-user",
|
|
"aud": "test-client",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"groups": groups,
|
|
})
|
|
token.Header["kid"] = "bearer-test-key"
|
|
signed, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
return signed
|
|
}
|
|
|
|
s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
|
|
authenticate := func(token string) (*IAMIdentity, s3err.ErrorCode) {
|
|
req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
return s3iam.AuthenticateJWT(ctx, req)
|
|
}
|
|
|
|
deniedToken := signJWT("denied-group")
|
|
|
|
// negative control: STS refuses to issue a session for this token and role
|
|
_, err = iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: "arn:aws:iam::role/BearerDeniedRole",
|
|
WebIdentityToken: deniedToken,
|
|
RoleSessionName: "trust-policy-check",
|
|
})
|
|
require.ErrorContains(t, err, "trust policy")
|
|
|
|
// the direct bearer path must reject the same token
|
|
identity, errCode := authenticate(deniedToken)
|
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
|
assert.Nil(t, identity)
|
|
|
|
// a role whose trust policy trusts the issuing provider still works
|
|
identity, errCode = authenticate(signJWT("trusted-group"))
|
|
require.Equal(t, s3err.ErrNone, errCode)
|
|
assert.Equal(t, "arn:aws:iam::role/BearerTrustedRole", identity.Principal)
|
|
}
|