mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 06:50:45 +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.
304 lines
8.8 KiB
Go
304 lines
8.8 KiB
Go
package sts
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
|
|
)
|
|
|
|
// ProviderFactory creates identity providers from configuration
|
|
type ProviderFactory struct{}
|
|
|
|
// NewProviderFactory creates a new provider factory
|
|
func NewProviderFactory() *ProviderFactory {
|
|
return &ProviderFactory{}
|
|
}
|
|
|
|
// CreateProvider creates an identity provider from configuration
|
|
func (f *ProviderFactory) CreateProvider(config *ProviderConfig) (providers.IdentityProvider, error) {
|
|
if config == nil {
|
|
return nil, fmt.Errorf(ErrConfigCannotBeNil)
|
|
}
|
|
|
|
if config.Name == "" {
|
|
return nil, fmt.Errorf(ErrProviderNameEmpty)
|
|
}
|
|
|
|
if config.Type == "" {
|
|
return nil, fmt.Errorf(ErrProviderTypeEmpty)
|
|
}
|
|
|
|
if !config.Enabled {
|
|
glog.V(2).Infof("Provider %s is disabled, skipping", config.Name)
|
|
return nil, nil
|
|
}
|
|
|
|
glog.V(2).Infof("Creating provider: name=%s, type=%s", config.Name, config.Type)
|
|
|
|
switch config.Type {
|
|
case ProviderTypeOIDC:
|
|
return f.createOIDCProvider(config)
|
|
case ProviderTypeLDAP:
|
|
return f.createLDAPProvider(config)
|
|
case ProviderTypeSAML:
|
|
return f.createSAMLProvider(config)
|
|
default:
|
|
return nil, fmt.Errorf(ErrUnsupportedProviderType, config.Type)
|
|
}
|
|
}
|
|
|
|
// createOIDCProvider creates an OIDC provider from configuration
|
|
func (f *ProviderFactory) createOIDCProvider(config *ProviderConfig) (providers.IdentityProvider, error) {
|
|
oidcConfig, err := f.convertToOIDCConfig(config.Config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert OIDC config: %w", err)
|
|
}
|
|
|
|
provider := oidc.NewOIDCProvider(config.Name)
|
|
if err := provider.Initialize(oidcConfig); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
|
}
|
|
|
|
return provider, nil
|
|
}
|
|
|
|
// createLDAPProvider creates an LDAP provider from configuration
|
|
func (f *ProviderFactory) createLDAPProvider(config *ProviderConfig) (providers.IdentityProvider, error) {
|
|
provider := ldap.NewLDAPProvider(config.Name)
|
|
if err := provider.Initialize(config.Config); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize LDAP provider: %w", err)
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
// createSAMLProvider creates a SAML provider from configuration
|
|
func (f *ProviderFactory) createSAMLProvider(config *ProviderConfig) (providers.IdentityProvider, error) {
|
|
// TODO: Implement SAML provider when available
|
|
return nil, fmt.Errorf("SAML provider not implemented yet")
|
|
}
|
|
|
|
// convertToOIDCConfig converts generic config map to OIDC config struct
|
|
func (f *ProviderFactory) convertToOIDCConfig(configMap map[string]interface{}) (*oidc.OIDCConfig, error) {
|
|
config := &oidc.OIDCConfig{}
|
|
|
|
// Required fields
|
|
if issuer, ok := configMap[ConfigFieldIssuer].(string); ok {
|
|
config.Issuer = issuer
|
|
} else {
|
|
return nil, fmt.Errorf(ErrIssuerRequired)
|
|
}
|
|
|
|
clientIDPresent := false
|
|
if clientID, ok := configMap[ConfigFieldClientID].(string); ok && clientID != "" {
|
|
config.ClientID = clientID
|
|
clientIDPresent = true
|
|
}
|
|
// Accept the AWS-compatible plural form. Either field satisfies the
|
|
// "at least one client id" requirement; both can be set together.
|
|
if rawList, ok := configMap[ConfigFieldClientIDs]; ok {
|
|
if list, err := f.convertToStringSlice(rawList); err == nil && len(list) > 0 {
|
|
config.ClientIDs = list
|
|
clientIDPresent = true
|
|
}
|
|
}
|
|
if !clientIDPresent {
|
|
return nil, fmt.Errorf(ErrClientIDRequired)
|
|
}
|
|
|
|
// Optional fields
|
|
if clientSecret, ok := configMap[ConfigFieldClientSecret].(string); ok {
|
|
config.ClientSecret = clientSecret
|
|
}
|
|
|
|
if jwksUri, ok := configMap[ConfigFieldJWKSUri].(string); ok {
|
|
config.JWKSUri = jwksUri
|
|
}
|
|
|
|
if userInfoUri, ok := configMap[ConfigFieldUserInfoUri].(string); ok {
|
|
config.UserInfoUri = userInfoUri
|
|
}
|
|
|
|
// Convert scopes array
|
|
if scopesInterface, ok := configMap[ConfigFieldScopes]; ok {
|
|
scopes, err := f.convertToStringSlice(scopesInterface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert scopes: %w", err)
|
|
}
|
|
config.Scopes = scopes
|
|
}
|
|
|
|
if tlsCaCert, ok := configMap[ConfigFieldTLSCACert].(string); ok {
|
|
config.TLSCACert = tlsCaCert
|
|
}
|
|
|
|
if rawThumbprints, ok := configMap[ConfigFieldThumbprints]; ok {
|
|
if list, err := f.convertToStringSlice(rawThumbprints); err == nil {
|
|
config.Thumbprints = list
|
|
}
|
|
}
|
|
|
|
if rawAllowed, ok := configMap[ConfigFieldAllowedPrincipalTagKeys]; ok {
|
|
if list, err := f.convertToStringSlice(rawAllowed); err == nil {
|
|
config.AllowedPrincipalTagKeys = list
|
|
}
|
|
}
|
|
|
|
if policyClaim, ok := configMap[ConfigFieldPolicyClaim].(string); ok {
|
|
config.PolicyClaim = policyClaim
|
|
}
|
|
|
|
if tlsInsecureSkipVerify, ok := configMap[ConfigFieldTLSInsecureSkipVerify].(bool); ok {
|
|
config.TLSInsecureSkipVerify = tlsInsecureSkipVerify
|
|
}
|
|
|
|
// Convert claims mapping
|
|
if claimsMapInterface, ok := configMap["claimsMapping"]; ok {
|
|
claimsMap, err := f.convertToStringMap(claimsMapInterface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert claimsMapping: %w", err)
|
|
}
|
|
config.ClaimsMapping = claimsMap
|
|
}
|
|
|
|
// Convert role mapping
|
|
if roleMappingInterface, ok := configMap["roleMapping"]; ok {
|
|
roleMapping, err := f.convertToRoleMapping(roleMappingInterface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert roleMapping: %w", err)
|
|
}
|
|
config.RoleMapping = roleMapping
|
|
}
|
|
|
|
glog.V(3).Infof("Converted OIDC config: issuer=%s, clientId=%s, jwksUri=%s",
|
|
config.Issuer, config.ClientID, config.JWKSUri)
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// convertToStringSlice converts interface{} to []string
|
|
func (f *ProviderFactory) convertToStringSlice(value interface{}) ([]string, error) {
|
|
switch v := value.(type) {
|
|
case []string:
|
|
return v, nil
|
|
case []interface{}:
|
|
result := make([]string, len(v))
|
|
for i, item := range v {
|
|
if str, ok := item.(string); ok {
|
|
result[i] = str
|
|
} else {
|
|
return nil, fmt.Errorf("non-string item in slice: %v", item)
|
|
}
|
|
}
|
|
return result, nil
|
|
default:
|
|
return nil, fmt.Errorf("cannot convert %T to []string", value)
|
|
}
|
|
}
|
|
|
|
// convertToStringMap converts interface{} to map[string]string
|
|
func (f *ProviderFactory) convertToStringMap(value interface{}) (map[string]string, error) {
|
|
switch v := value.(type) {
|
|
case map[string]string:
|
|
return v, nil
|
|
case map[string]interface{}:
|
|
result := make(map[string]string)
|
|
for key, val := range v {
|
|
if str, ok := val.(string); ok {
|
|
result[key] = str
|
|
} else {
|
|
return nil, fmt.Errorf("non-string value for key %s: %v", key, val)
|
|
}
|
|
}
|
|
return result, nil
|
|
default:
|
|
return nil, fmt.Errorf("cannot convert %T to map[string]string", value)
|
|
}
|
|
}
|
|
|
|
// LoadProvidersFromConfig creates providers from configuration
|
|
func (f *ProviderFactory) LoadProvidersFromConfig(configs []*ProviderConfig) (map[string]providers.IdentityProvider, error) {
|
|
providersMap := make(map[string]providers.IdentityProvider)
|
|
|
|
for _, config := range configs {
|
|
if config == nil {
|
|
glog.V(1).Infof("Skipping nil provider config")
|
|
continue
|
|
}
|
|
|
|
glog.V(2).Infof("Loading provider: %s (type: %s, enabled: %t)",
|
|
config.Name, config.Type, config.Enabled)
|
|
|
|
if !config.Enabled {
|
|
glog.V(2).Infof("Provider %s is disabled, skipping", config.Name)
|
|
continue
|
|
}
|
|
|
|
provider, err := f.CreateProvider(config)
|
|
if err != nil {
|
|
glog.Errorf("Failed to create provider %s: %v", config.Name, err)
|
|
return nil, fmt.Errorf("failed to create provider %s: %w", config.Name, err)
|
|
}
|
|
|
|
if provider != nil {
|
|
providersMap[config.Name] = provider
|
|
glog.V(1).Infof("Successfully loaded provider: %s", config.Name)
|
|
}
|
|
}
|
|
|
|
glog.V(1).Infof("Loaded %d identity providers from configuration", len(providersMap))
|
|
return providersMap, nil
|
|
}
|
|
|
|
// convertToRoleMapping converts interface{} to *providers.RoleMapping
|
|
func (f *ProviderFactory) convertToRoleMapping(value interface{}) (*providers.RoleMapping, error) {
|
|
roleMappingMap, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("roleMapping must be an object")
|
|
}
|
|
|
|
roleMapping := &providers.RoleMapping{}
|
|
|
|
// Convert rules
|
|
if rulesInterface, ok := roleMappingMap["rules"]; ok {
|
|
rulesSlice, ok := rulesInterface.([]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("rules must be an array")
|
|
}
|
|
|
|
rules := make([]providers.MappingRule, len(rulesSlice))
|
|
for i, ruleInterface := range rulesSlice {
|
|
ruleMap, ok := ruleInterface.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("rule must be an object")
|
|
}
|
|
|
|
rule := providers.MappingRule{}
|
|
if claim, ok := ruleMap["claim"].(string); ok {
|
|
rule.Claim = claim
|
|
}
|
|
if value, ok := ruleMap["value"].(string); ok {
|
|
rule.Value = value
|
|
}
|
|
if role, ok := ruleMap["role"].(string); ok {
|
|
rule.Role = role
|
|
}
|
|
if condition, ok := ruleMap["condition"].(string); ok {
|
|
rule.Condition = condition
|
|
}
|
|
|
|
rules[i] = rule
|
|
}
|
|
roleMapping.Rules = rules
|
|
}
|
|
|
|
// Convert default role
|
|
if defaultRole, ok := roleMappingMap["defaultRole"].(string); ok {
|
|
roleMapping.DefaultRole = defaultRole
|
|
}
|
|
|
|
return roleMapping, nil
|
|
}
|