mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 23:10:44 +01:00
* iam/oidc: drop single-use web-identity tokens and the jti replay cache Reverts the single-use semantics added in #11936 (and the #12017 refactor): AssumeRoleWithWebIdentity consumed each web-identity token's jti into a bounded replay cache, so a second exchange of the same token was rejected until exp + skew. AWS STS accepts a web-identity token repeatedly until it expires, and Kubernetes workload identity depends on that: the kubelet keeps a projected ServiceAccount token until ~80% of its lifetime, so pod restarts, co-located containers sharing the token file, repeated CLI invocations, and pre-rotation SDK refreshes all re-present the same jti and were refused on 4.50. The replay machinery also carried a real maintenance cost — a bounded concurrent cache, fingerprint-keyed entries for jti-less tokens, adopt/merge handoffs so provider rebuilds could not reopen consumed tokens, and provider-reuse bookkeeping in the refresh path that existed only to preserve that state. Removing it restores AWS semantics and deletes the whole apparatus. Refs #12063. Alternative to the per-provider opt-out in #12064. * iam: reuse unchanged OIDC providers on refresh refreshOIDCProvidersFrom rebuilt every managed provider on any store change, so an unrelated write discarded each provider's JWKS cache and an unreachable issuer could then fail exchanges the cached keys would have allowed. Keep a per-record provider cache keyed on the record content fingerprint and only rebuild when the record changes. * iam/oidc: require exp in ValidateToken Dropping the single-use check also removed the implicit bound on expiry-less tokens: a signed web-identity token without exp could mint STS sessions indefinitely. Require the exp claim, matching AWS AssumeRoleWithWebIdentity which rejects tokens without one. * iam/oidc: bound idle connections on the provider transport A provider replaced on refresh drops its http.Transport without a close; give it an IdleConnTimeout so pooled connections and their goroutines cannot linger. * s3api: cover web-identity token reuse over HTTP Sign one token and exchange that exact string twice, directly covering the Kubernetes projected-token reuse this change restores. * test(iam): refresh reuses an unchanged record's provider Covers the provider-reuse cache: an unrelated record's refresh keeps a provider's JWKS state, a changed record rebuilds it.
167 lines
5.4 KiB
Go
167 lines
5.4 KiB
Go
package sts
|
|
|
|
import (
|
|
"errors"
|
|
)
|
|
|
|
// Store Types
|
|
const (
|
|
StoreTypeMemory = "memory"
|
|
StoreTypeFiler = "filer"
|
|
StoreTypeRedis = "redis"
|
|
)
|
|
|
|
// Provider Types
|
|
const (
|
|
ProviderTypeOIDC = "oidc"
|
|
ProviderTypeLDAP = "ldap"
|
|
ProviderTypeSAML = "saml"
|
|
)
|
|
|
|
// Policy Effects
|
|
const (
|
|
EffectAllow = "Allow"
|
|
EffectDeny = "Deny"
|
|
)
|
|
|
|
// Default Paths - aligned with filer /etc/ convention
|
|
const (
|
|
DefaultSessionBasePath = "/etc/iam/sessions"
|
|
DefaultPolicyBasePath = "/etc/iam/policies"
|
|
DefaultRoleBasePath = "/etc/iam/roles"
|
|
)
|
|
|
|
// Default Values
|
|
const (
|
|
DefaultTokenDuration = 3600 // 1 hour in seconds
|
|
DefaultMaxSessionLength = 43200 // 12 hours in seconds
|
|
DefaultIssuer = "seaweedfs-sts"
|
|
DefaultStoreType = StoreTypeFiler // Default store type for persistence
|
|
MinSigningKeyLength = 16 // Minimum signing key length in bytes
|
|
)
|
|
|
|
// Configuration Field Names
|
|
const (
|
|
ConfigFieldFilerAddress = "filerAddress"
|
|
ConfigFieldBasePath = "basePath"
|
|
ConfigFieldIssuer = "issuer"
|
|
ConfigFieldClientID = "clientId"
|
|
ConfigFieldClientIDs = "clientIds"
|
|
ConfigFieldThumbprints = "thumbprints"
|
|
ConfigFieldAllowedPrincipalTagKeys = "allowedPrincipalTagKeys"
|
|
ConfigFieldPolicyClaim = "policyClaim"
|
|
ConfigFieldClientSecret = "clientSecret"
|
|
ConfigFieldJWKSUri = "jwksUri"
|
|
ConfigFieldScopes = "scopes"
|
|
ConfigFieldUserInfoUri = "userInfoUri"
|
|
ConfigFieldRedirectUri = "redirectUri"
|
|
ConfigFieldTLSCACert = "tlsCaCert"
|
|
ConfigFieldTLSInsecureSkipVerify = "tlsInsecureSkipVerify"
|
|
)
|
|
|
|
// Error Messages
|
|
const (
|
|
ErrConfigCannotBeNil = "config cannot be nil"
|
|
ErrProviderCannotBeNil = "provider cannot be nil"
|
|
ErrProviderNameEmpty = "provider name cannot be empty"
|
|
ErrProviderTypeEmpty = "provider type cannot be empty"
|
|
ErrTokenCannotBeEmpty = "token cannot be empty"
|
|
ErrSessionTokenCannotBeEmpty = "session token cannot be empty"
|
|
ErrSessionIDCannotBeEmpty = "session ID cannot be empty"
|
|
ErrSTSServiceNotInitialized = "STS service not initialized"
|
|
ErrProviderNotInitialized = "provider not initialized"
|
|
ErrInvalidTokenDuration = "token duration must be positive"
|
|
ErrInvalidMaxSessionLength = "max session length must be positive"
|
|
ErrIssuerRequired = "issuer is required"
|
|
ErrSigningKeyTooShort = "signing key must be at least %d bytes"
|
|
ErrFilerAddressRequired = "filer address is required"
|
|
ErrClientIDRequired = "clientId is required for OIDC provider"
|
|
ErrUnsupportedStoreType = "unsupported store type: %s"
|
|
ErrUnsupportedProviderType = "unsupported provider type: %s"
|
|
ErrInvalidTokenFormat = "invalid session token format: %w"
|
|
ErrSessionValidationFailed = "session validation failed: %w"
|
|
ErrInvalidToken = "invalid token: %w"
|
|
ErrTokenNotValid = "token is not valid"
|
|
ErrInvalidTokenClaims = "invalid token claims"
|
|
ErrInvalidIssuer = "invalid issuer"
|
|
ErrMissingSessionID = "missing session ID"
|
|
)
|
|
|
|
// Typed errors for robust error checking with errors.Is()
|
|
// These enable the HTTP layer to use errors.Is() instead of fragile string matching
|
|
var (
|
|
// ErrTokenExpired indicates that the provided token has expired
|
|
ErrTypedTokenExpired = errors.New("token has expired")
|
|
|
|
// ErrTypedInvalidToken indicates that the token format is invalid or malformed
|
|
ErrTypedInvalidToken = errors.New("invalid token format")
|
|
|
|
// ErrTypedInvalidIssuer indicates that the token issuer is not trusted
|
|
ErrTypedInvalidIssuer = errors.New("invalid token issuer")
|
|
|
|
// ErrTypedInvalidAudience indicates that the token audience doesn't match expected value
|
|
ErrTypedInvalidAudience = errors.New("invalid token audience")
|
|
|
|
// ErrTypedMissingClaims indicates that required claims are missing from the token
|
|
ErrTypedMissingClaims = errors.New("missing required claims")
|
|
)
|
|
|
|
// JWT Claims
|
|
const (
|
|
JWTClaimIssuer = "iss"
|
|
JWTClaimSubject = "sub"
|
|
JWTClaimAudience = "aud"
|
|
JWTClaimExpiration = "exp"
|
|
JWTClaimIssuedAt = "iat"
|
|
JWTClaimTokenType = "token_type"
|
|
)
|
|
|
|
// Token Types
|
|
const (
|
|
TokenTypeSession = "session"
|
|
TokenTypeAccess = "access"
|
|
TokenTypeRefresh = "refresh"
|
|
)
|
|
|
|
// AWS STS Actions
|
|
const (
|
|
ActionAssumeRole = "sts:AssumeRole"
|
|
ActionAssumeRoleWithWebIdentity = "sts:AssumeRoleWithWebIdentity"
|
|
ActionAssumeRoleWithCredentials = "sts:AssumeRoleWithCredentials"
|
|
ActionGetFederationToken = "sts:GetFederationToken"
|
|
ActionValidateSession = "sts:ValidateSession"
|
|
)
|
|
|
|
// Session File Prefixes
|
|
const (
|
|
SessionFilePrefix = "session_"
|
|
SessionFileExt = ".json"
|
|
PolicyFilePrefix = "policy_"
|
|
PolicyFileExt = ".json"
|
|
RoleFileExt = ".json"
|
|
)
|
|
|
|
// HTTP Headers
|
|
const (
|
|
HeaderAuthorization = "Authorization"
|
|
HeaderContentType = "Content-Type"
|
|
HeaderUserAgent = "User-Agent"
|
|
)
|
|
|
|
// Content Types
|
|
const (
|
|
ContentTypeJSON = "application/json"
|
|
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
|
|
)
|
|
|
|
// Default Test Values
|
|
const (
|
|
TestSigningKey32Chars = "test-signing-key-32-characters-long"
|
|
TestIssuer = "test-sts"
|
|
TestClientID = "test-client"
|
|
TestSessionID = "test-session-123"
|
|
TestValidToken = "valid_test_token"
|
|
TestInvalidToken = "invalid_token"
|
|
TestExpiredToken = "expired_token"
|
|
)
|