diff --git a/test/s3/iam/iam_config.json b/test/s3/iam/iam_config.json index ce261fbda..ac02184a7 100644 --- a/test/s3/iam/iam_config.json +++ b/test/s3/iam/iam_config.json @@ -5,13 +5,51 @@ "issuer": "seaweedfs-sts", "signingKey": "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc=" }, - "identityProviders": [ + "providers": [ { "name": "test-oidc", "type": "mock", "config": { "issuer": "test-oidc-issuer" } + }, + { + "name": "keycloak", + "type": "oidc", + "enabled": true, + "config": { + "issuer": "http://localhost:8080/realms/seaweedfs-test", + "clientId": "seaweedfs-s3", + "clientSecret": "seaweedfs-s3-secret", + "jwksUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/certs", + "userInfoUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/userinfo", + "scopes": ["openid", "profile", "email"], + "claimsMapping": { + "username": "preferred_username", + "email": "email", + "name": "name" + }, + "roleMapping": { + "rules": [ + { + "claim": "roles", + "value": "s3-admin", + "role": "arn:seaweed:iam::role/KeycloakAdminRole" + }, + { + "claim": "roles", + "value": "s3-read-only", + "role": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + }, + { + "claim": "roles", + "value": "s3-write-only", + "role": "arn:seaweed:iam::role/KeycloakWriteOnlyRole" + } + ], + "defaultRole": "arn:seaweed:iam::role/KeycloakReadOnlyRole" + } + } } ], "policy": { @@ -72,6 +110,60 @@ }, "attachedPolicies": ["S3WriteOnlyPolicy"], "description": "Write-only role for testing" + }, + { + "roleName": "KeycloakAdminRole", + "roleArn": "arn:seaweed:iam::role/KeycloakAdminRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3AdminPolicy"], + "description": "Admin role for Keycloak users" + }, + { + "roleName": "KeycloakReadOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakReadOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3ReadOnlyPolicy"], + "description": "Read-only role for Keycloak users" + }, + { + "roleName": "KeycloakWriteOnlyRole", + "roleArn": "arn:seaweed:iam::role/KeycloakWriteOnlyRole", + "trustPolicy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "keycloak" + }, + "Action": ["sts:AssumeRoleWithWebIdentity"] + } + ] + }, + "attachedPolicies": ["S3WriteOnlyPolicy"], + "description": "Write-only role for Keycloak users" } ], "policies": [ diff --git a/test/s3/iam/s3_iam_framework.go b/test/s3/iam/s3_iam_framework.go index 675c5a989..f8d1d2b69 100644 --- a/test/s3/iam/s3_iam_framework.go +++ b/test/s3/iam/s3_iam_framework.go @@ -114,9 +114,10 @@ func NewKeycloakClient(baseURL, realm, clientID, clientSecret string) *KeycloakC // isKeycloakAvailable checks if Keycloak is running and accessible func (f *S3IAMTestFramework) isKeycloakAvailable(keycloakURL string) bool { client := &http.Client{Timeout: 5 * time.Second} - healthURL := fmt.Sprintf("%s/health/ready", keycloakURL) + // Use realms endpoint instead of health/ready for Keycloak v26+ + realmsURL := fmt.Sprintf("%s/realms/master", keycloakURL) - resp, err := client.Get(healthURL) + resp, err := client.Get(realmsURL) if err != nil { return false } @@ -268,16 +269,40 @@ func (t *BearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, err // Clone the request to avoid modifying the original newReq := req.Clone(req.Context()) - // Add Bearer token authorization header - newReq.Header.Set("Authorization", "Bearer "+t.Token) - - // Remove AWS signature headers if present (they conflict with Bearer auth) + // Remove ALL existing Authorization headers first to prevent conflicts + newReq.Header.Del("Authorization") newReq.Header.Del("X-Amz-Date") newReq.Header.Del("X-Amz-Content-Sha256") newReq.Header.Del("X-Amz-Signature") newReq.Header.Del("X-Amz-Algorithm") newReq.Header.Del("X-Amz-Credential") newReq.Header.Del("X-Amz-SignedHeaders") + newReq.Header.Del("X-Amz-Security-Token") + + // Add Bearer token authorization header + newReq.Header.Set("Authorization", "Bearer "+t.Token) + + // Debug: log the token being sent (first 50 chars) and subject + tokenPreview := t.Token + if len(tokenPreview) > 50 { + tokenPreview = tokenPreview[:50] + "..." + } + + // Debug logging can be enabled if needed + // Extract subject from token for debugging + // parts := strings.Split(t.Token, ".") + // subject := "unknown" + // if len(parts) >= 2 { + // if decoded, err := base64.RawURLEncoding.DecodeString(parts[1]); err == nil { + // var claims map[string]interface{} + // if json.Unmarshal(decoded, &claims) == nil { + // if sub, ok := claims["sub"].(string); ok { + // subject = sub[:8] + "..." // First 8 chars of subject + // } + // } + // } + // } + // fmt.Printf("DEBUG: Sending Bearer token (subject: %s): %s\n", subject, tokenPreview) // Use underlying transport transport := t.Transport @@ -464,6 +489,79 @@ func (f *S3IAMTestFramework) CreateS3ClientWithSessionToken(sessionToken string) return s3.New(sess), nil } +// CreateS3ClientWithKeycloakToken creates an S3 client using a Keycloak JWT token +func (f *S3IAMTestFramework) CreateS3ClientWithKeycloakToken(keycloakToken string) (*s3.S3, error) { + // Create a fresh HTTP transport with aggressive timeouts to prevent hanging + transport := &http.Transport{ + DisableKeepAlives: true, // Force new connections for each request + DisableCompression: true, // Disable compression to simplify requests + MaxIdleConns: 0, // No connection pooling + MaxIdleConnsPerHost: 0, // No connection pooling per host + IdleConnTimeout: 1 * time.Second, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + // Create a custom HTTP client with aggressive timeouts + httpClient := &http.Client{ + Timeout: 30 * time.Second, // Overall request timeout + Transport: &BearerTokenTransport{ + Token: keycloakToken, + Transport: transport, + }, + } + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(TestRegion), + Endpoint: aws.String(TestS3Endpoint), + Credentials: credentials.AnonymousCredentials, + DisableSSL: aws.Bool(true), + S3ForcePathStyle: aws.Bool(true), + HTTPClient: httpClient, + MaxRetries: aws.Int(0), // No retries to avoid delays + }) + if err != nil { + return nil, fmt.Errorf("failed to create AWS session: %v", err) + } + + return s3.New(sess), nil +} + +// TestKeycloakTokenDirectly tests a Keycloak token with direct HTTP request (bypassing AWS SDK) +func (f *S3IAMTestFramework) TestKeycloakTokenDirectly(keycloakToken string) error { + // Create a simple HTTP client with timeout + client := &http.Client{ + Timeout: 10 * time.Second, + } + + // Create request to list buckets + req, err := http.NewRequest("GET", TestS3Endpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + + // Add Bearer token + req.Header.Set("Authorization", "Bearer "+keycloakToken) + req.Header.Set("Host", "localhost:8333") + + // Make request + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("request failed: %v", err) + } + defer resp.Body.Close() + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %v", err) + } + + fmt.Printf("Direct HTTP test - Status: %d, Body: %s\n", resp.StatusCode, string(body)) + return nil +} + // generateJWTToken creates a JWT token for testing func (f *S3IAMTestFramework) generateJWTToken(username, roleName string, validDuration time.Duration) (string, error) { now := time.Now() diff --git a/test/s3/iam/s3_keycloak_integration_test.go b/test/s3/iam/s3_keycloak_integration_test.go index 007097c87..09203f357 100644 --- a/test/s3/iam/s3_keycloak_integration_test.go +++ b/test/s3/iam/s3_keycloak_integration_test.go @@ -1,7 +1,10 @@ package iam import ( + "encoding/base64" + "encoding/json" "os" + "strings" "testing" "github.com/aws/aws-sdk-go/service/s3" @@ -43,7 +46,7 @@ func TestKeycloakAuthentication(t *testing.T) { assert.NotEmpty(t, token, "JWT token should not be empty") // Verify token can be used to create S3 client - s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole") + s3Client, err := framework.CreateS3ClientWithKeycloakToken(token) require.NoError(t, err) assert.NotNil(t, s3Client, "S3 client should be created successfully") @@ -71,17 +74,42 @@ func TestKeycloakAuthentication(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, token, "JWT token should not be empty") - // Create S3 client with read-only user - s3Client, err := framework.CreateS3ClientWithJWT("read-user", "S3ReadOnlyRole") - require.NoError(t, err) + // Debug: decode token to verify it's for read-user + parts := strings.Split(token, ".") + if len(parts) >= 2 { + payload := parts[1] + // Add padding if needed + for len(payload)%4 != 0 { + payload += "=" + } + decoded, err := base64.StdEncoding.DecodeString(payload) + if err == nil { + var claims map[string]interface{} + if json.Unmarshal(decoded, &claims) == nil { + t.Logf("Token username: %v", claims["preferred_username"]) + t.Logf("Token roles: %v", claims["roles"]) + } + } + } - // Test that read-only user can list buckets - _, err = s3Client.ListBuckets(&s3.ListBucketsInput{}) - assert.NoError(t, err, "Read-only user should be able to list buckets") + // First test with direct HTTP request to verify OIDC authentication works + t.Logf("Testing with direct HTTP request...") + err = framework.TestKeycloakTokenDirectly(token) + require.NoError(t, err, "Direct HTTP test should succeed") - // Test that read-only user cannot create buckets - err = framework.CreateBucket(s3Client, testKeycloakBucket+"-readonly") - assert.Error(t, err, "Read-only user should not be able to create buckets") + // Create S3 client with Keycloak token + s3Client, err := framework.CreateS3ClientWithKeycloakToken(token) + require.NoError(t, err) + + // Test that read-only user can list buckets + t.Logf("Testing ListBuckets with AWS SDK...") + _, err = s3Client.ListBuckets(&s3.ListBucketsInput{}) + assert.NoError(t, err, "Read-only user should be able to list buckets") + + // Test that read-only user cannot create buckets + t.Logf("Testing CreateBucket with AWS SDK...") + err = framework.CreateBucket(s3Client, testKeycloakBucket+"-readonly") + assert.Error(t, err, "Read-only user should not be able to create buckets") }) t.Run("invalid_user_authentication", func(t *testing.T) { @@ -110,7 +138,10 @@ func TestKeycloakTokenExpiration(t *testing.T) { assert.Greater(t, tokenResp.ExpiresIn, 0, "Token should have expiration time") // Test that token works initially - s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole") + token, err := framework.getKeycloakToken("admin-user") + require.NoError(t, err) + + s3Client, err := framework.CreateS3ClientWithKeycloakToken(token) require.NoError(t, err) _, err = s3Client.ListBuckets(&s3.ListBucketsInput{}) @@ -158,8 +189,12 @@ func TestKeycloakRoleMapping(t *testing.T) { for _, tc := range testCases { t.Run(tc.username, func(t *testing.T) { - // Create S3 client for the user - s3Client, err := framework.CreateS3ClientWithJWT(tc.username, tc.expectedRole) + // Get Keycloak token for the user + token, err := framework.getKeycloakToken(tc.username) + require.NoError(t, err) + + // Create S3 client with Keycloak token + s3Client, err := framework.CreateS3ClientWithKeycloakToken(token) require.NoError(t, err, tc.description) // Test list buckets permission @@ -192,7 +227,10 @@ func TestKeycloakS3Operations(t *testing.T) { } // Use admin user for comprehensive testing - s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole") + token, err := framework.getKeycloakToken("admin-user") + require.NoError(t, err) + + s3Client, err := framework.CreateS3ClientWithKeycloakToken(token) require.NoError(t, err) bucketName := testKeycloakBucket + "-operations" diff --git a/weed/iam/oidc/oidc_provider.go b/weed/iam/oidc/oidc_provider.go index f6a81eb90..3fbe6ce85 100644 --- a/weed/iam/oidc/oidc_provider.go +++ b/weed/iam/oidc/oidc_provider.go @@ -144,11 +144,22 @@ func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*provide displayName, _ := claims.GetClaimString("name") groups, _ := claims.GetClaimStringSlice("groups") + // Map claims to roles using configured role mapping + roles := p.mapClaimsToRolesWithConfig(claims) + + // Create attributes map and add roles + attributes := make(map[string]string) + if len(roles) > 0 { + // Store roles as a comma-separated string in attributes + attributes["roles"] = strings.Join(roles, ",") + } + return &providers.ExternalIdentity{ UserID: claims.Subject, Email: email, DisplayName: displayName, Groups: groups, + Attributes: attributes, Provider: p.name, }, nil } @@ -288,11 +299,20 @@ func (p *OIDCProvider) ValidateToken(ctx context.Context, token string) (*provid return nil, fmt.Errorf("invalid or missing issuer claim") } - audience, ok := claims["aud"].(string) - if !ok || audience != p.config.ClientID { + // Check audience claim (aud) or authorized party (azp) - Keycloak uses azp + var audience string + if aud, ok := claims["aud"].(string); ok { + audience = aud + } else if azp, ok := claims["azp"].(string); ok { + audience = azp + } else { return nil, fmt.Errorf("invalid or missing audience claim") } + if audience != p.config.ClientID { + return nil, fmt.Errorf("invalid audience claim: expected %s, got %s", p.config.ClientID, audience) + } + subject, ok := claims["sub"].(string) if !ok { return nil, fmt.Errorf("missing subject claim") @@ -313,7 +333,7 @@ func (p *OIDCProvider) ValidateToken(ctx context.Context, token string) (*provid return tokenClaims, nil } -// mapClaimsToRoles maps token claims to SeaweedFS roles +// mapClaimsToRoles maps token claims to SeaweedFS roles (legacy method) func (p *OIDCProvider) mapClaimsToRoles(claims *providers.TokenClaims) []string { roles := []string{} @@ -339,6 +359,30 @@ func (p *OIDCProvider) mapClaimsToRoles(claims *providers.TokenClaims) []string return roles } +// mapClaimsToRolesWithConfig maps token claims to roles using configured role mapping +func (p *OIDCProvider) mapClaimsToRolesWithConfig(claims *providers.TokenClaims) []string { + if p.config.RoleMapping == nil { + // Fallback to legacy mapping if no role mapping configured + return p.mapClaimsToRoles(claims) + } + + roles := []string{} + + // Apply role mapping rules + for _, rule := range p.config.RoleMapping.Rules { + if rule.Matches(claims) { + roles = append(roles, rule.Role) + } + } + + // Use default role if no rules matched + if len(roles) == 0 && p.config.RoleMapping.DefaultRole != "" { + roles = []string{p.config.RoleMapping.DefaultRole} + } + + return roles +} + // getPublicKey retrieves the public key for the given key ID from JWKS func (p *OIDCProvider) getPublicKey(ctx context.Context, kid string) (interface{}, error) { // Fetch JWKS if not cached or refresh if needed diff --git a/weed/iam/sts/provider_factory.go b/weed/iam/sts/provider_factory.go index a13801f18..3b394f7af 100644 --- a/weed/iam/sts/provider_factory.go +++ b/weed/iam/sts/provider_factory.go @@ -145,6 +145,13 @@ func (f *ProviderFactory) convertToOIDCConfig(configMap map[string]interface{}) } } + // Convert role mapping + if roleMappingInterface, ok := configMap["roleMapping"]; ok { + if roleMapping, err := f.convertToRoleMapping(roleMappingInterface); err == nil { + config.RoleMapping = roleMapping + } + } + glog.V(3).Infof("Converted OIDC config: issuer=%s, clientId=%s, jwksUri=%s", config.Issuer, config.ClientID, config.JWKSUri) @@ -225,6 +232,56 @@ func (f *ProviderFactory) LoadProvidersFromConfig(configs []*ProviderConfig) (ma 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 +} + // ValidateProviderConfig validates a provider configuration func (f *ProviderFactory) ValidateProviderConfig(config *ProviderConfig) error { if config == nil { @@ -263,7 +320,7 @@ func (f *ProviderFactory) validateOIDCConfig(config map[string]interface{}) erro if _, ok := config[ConfigFieldIssuer]; !ok { return fmt.Errorf("OIDC provider requires '%s' field", ConfigFieldIssuer) } - + if _, ok := config[ConfigFieldClientID]; !ok { return fmt.Errorf("OIDC provider requires '%s' field", ConfigFieldClientID) } diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index 7845dcd52..05999aff0 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -299,6 +299,11 @@ func (s *STSService) RegisterProvider(provider providers.IdentityProvider) error return nil } +// GetProviders returns all registered identity providers +func (s *STSService) GetProviders() map[string]providers.IdentityProvider { + return s.providers +} + // AssumeRoleWithWebIdentity assumes a role using a web identity token (OIDC) // This method is now completely stateless - all session information is embedded in the JWT token func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *AssumeRoleWithWebIdentityRequest) (*AssumeRoleResponse, error) { diff --git a/weed/s3api/s3_iam_middleware.go b/weed/s3api/s3_iam_middleware.go index 521ca63a6..10f4eea02 100644 --- a/weed/s3api/s3_iam_middleware.go +++ b/weed/s3api/s3_iam_middleware.go @@ -10,6 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam/integration" + "github.com/seaweedfs/seaweedfs/weed/iam/providers" "github.com/seaweedfs/seaweedfs/weed/iam/sts" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" @@ -61,18 +62,36 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ return nil, s3err.ErrAccessDenied } - // Parse JWT token to extract claims + // Try to parse as STS session token first tokenClaims, err := parseJWTToken(sessionToken) if err != nil { glog.V(3).Infof("Failed to parse JWT token: %v", err) return nil, s3err.ErrAccessDenied } - // Extract role information from token claims + // Check if this is an STS session token (has "role" claim) roleName, ok := tokenClaims["role"].(string) if !ok || roleName == "" { - glog.V(0).Info("No role found in JWT token") - return nil, s3err.ErrAccessDenied + // Not an STS session token, try to validate as OIDC token + identity, err := s3iam.validateOIDCToken(ctx, sessionToken) + if err != nil { + glog.V(0).Infof("Failed to validate as OIDC token: %v", err) + return nil, s3err.ErrAccessDenied + } + + // Extract role from OIDC identity + if identity.RoleArn == "" { + glog.V(0).Info("No role found in OIDC token") + return nil, s3err.ErrAccessDenied + } + + // Return IAM identity for OIDC token + return &IAMIdentity{ + Name: identity.UserID, + Principal: identity.RoleArn, + SessionToken: sessionToken, + Account: nil, // OIDC tokens don't have account info + }, s3err.ErrNone } sessionName, ok := tokenClaims["snam"].(string) @@ -409,3 +428,68 @@ func (enhanced *EnhancedS3ApiServer) AuthorizeRequest(r *http.Request, identity // Use our IAM integration for authorization return enhanced.iamIntegration.AuthorizeAction(ctx, iamIdentity, action, bucket, object, r) } + +// OIDCIdentity represents an identity validated through OIDC +type OIDCIdentity struct { + UserID string + RoleArn string + Provider string +} + +// validateOIDCToken validates an OIDC token using registered identity providers +func (s3iam *S3IAMIntegration) validateOIDCToken(ctx context.Context, token string) (*OIDCIdentity, error) { + if s3iam.iamManager == nil { + return nil, fmt.Errorf("IAM manager not available") + } + + // Get STS service to access identity providers + stsService := s3iam.iamManager.GetSTSService() + if stsService == nil { + return nil, fmt.Errorf("STS service not available") + } + + // Try to validate token with each registered OIDC provider + providers := stsService.GetProviders() + for providerName, provider := range providers { + // Try to authenticate with this provider + externalIdentity, err := provider.Authenticate(ctx, token) + if err != nil { + glog.V(3).Infof("Provider %s failed to authenticate token: %v", providerName, err) + continue + } + + // Extract role from external identity attributes + rolesAttr, exists := externalIdentity.Attributes["roles"] + if !exists || rolesAttr == "" { + glog.V(3).Infof("No roles found in external identity from provider %s", providerName) + continue + } + + // Parse roles (stored as comma-separated string) + roles := strings.Split(rolesAttr, ",") + if len(roles) == 0 { + glog.V(3).Infof("Empty roles list from provider %s", providerName) + continue + } + + // Use the first role as the primary role + roleArn := roles[0] + + return &OIDCIdentity{ + UserID: externalIdentity.UserID, + RoleArn: roleArn, + Provider: providerName, + }, nil + } + + return nil, fmt.Errorf("token not valid for any registered OIDC provider") +} + +// getProviderNames returns a list of provider names for debugging +func getProviderNames(providers map[string]providers.IdentityProvider) []string { + names := make([]string, 0, len(providers)) + for name := range providers { + names = append(names, name) + } + return names +} diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index 856449e85..8bd452b0e 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -450,6 +450,28 @@ func loadIAMManagerFromConfig(configPath string) (*integration.IAMManager, error return nil, fmt.Errorf("failed to initialize IAM manager: %w", err) } + // Load identity providers + providerFactory := sts.NewProviderFactory() + for _, providerConfig := range configRoot.Providers { + provider, err := providerFactory.CreateProvider(&sts.ProviderConfig{ + Name: providerConfig["name"].(string), + Type: providerConfig["type"].(string), + Enabled: true, + Config: providerConfig["config"].(map[string]interface{}), + }) + if err != nil { + glog.Warningf("Failed to create provider %s: %v", providerConfig["name"], err) + continue + } + if provider != nil { + if err := iamManager.RegisterIdentityProvider(provider); err != nil { + glog.Warningf("Failed to register provider %s: %v", providerConfig["name"], err) + } else { + glog.V(1).Infof("Registered identity provider: %s", providerConfig["name"]) + } + } + } + // Load policies for _, policyDef := range configRoot.Policies { if err := iamManager.CreatePolicy(context.Background(), policyDef.Name, policyDef.Document); err != nil { @@ -464,7 +486,7 @@ func loadIAMManagerFromConfig(configPath string) (*integration.IAMManager, error } } - glog.V(0).Infof("Loaded %d policies and %d roles from config", len(configRoot.Policies), len(configRoot.Roles)) + glog.V(0).Infof("Loaded %d providers, %d policies and %d roles from config", len(configRoot.Providers), len(configRoot.Policies), len(configRoot.Roles)) return iamManager, nil }