diff --git a/weed/iam/ldap/ldap_provider.go b/weed/iam/ldap/ldap_provider.go index 6458098f2..f970b330a 100644 --- a/weed/iam/ldap/ldap_provider.go +++ b/weed/iam/ldap/ldap_provider.go @@ -70,7 +70,7 @@ func (p *LDAPProvider) Initialize(config interface{}) error { if config == nil { return fmt.Errorf("config cannot be nil") } - + ldapConfig, ok := config.(*LDAPConfig) if !ok { return fmt.Errorf("invalid config type for LDAP provider") @@ -136,17 +136,17 @@ func (p *LDAPProvider) Authenticate(ctx context.Context, credentials string) (*p } username, password := parts[0], parts[1] - + // TODO: Implement actual LDAP authentication // 1. Connect to LDAP server using bind credentials // 2. Search for user using configured user filter // 3. Attempt to bind with user credentials // 4. Retrieve user attributes and group memberships // 5. Map to ExternalIdentity structure - + _ = username // Avoid unused variable warning _ = password // Avoid unused variable warning - + return nil, fmt.Errorf("LDAP authentication not implemented yet - requires LDAP client integration") } @@ -166,7 +166,7 @@ func (p *LDAPProvider) GetUserInfo(ctx context.Context, userID string) (*provide // 3. Retrieve configured attributes (email, displayName, etc.) // 4. Retrieve group memberships using group filter // 5. Map to ExternalIdentity structure - + return nil, fmt.Errorf("LDAP user info retrieval not implemented yet") } @@ -195,10 +195,10 @@ func (p *LDAPProvider) ValidateToken(ctx context.Context, token string) (*provid // 4. Attempt to bind with user credentials to validate password // 5. Extract user claims (DN, attributes, group memberships) // 6. Return TokenClaims with LDAP-specific information - + _ = username // Avoid unused variable warning _ = password // Avoid unused variable warning - + return nil, fmt.Errorf("LDAP credential validation not implemented yet") } diff --git a/weed/iam/ldap/mock_provider.go b/weed/iam/ldap/mock_provider.go index d0b532898..b748a1f72 100644 --- a/weed/iam/ldap/mock_provider.go +++ b/weed/iam/ldap/mock_provider.go @@ -11,7 +11,7 @@ import ( // MockLDAPProvider is a mock implementation for testing type MockLDAPProvider struct { *LDAPProvider - TestUsers map[string]*providers.ExternalIdentity + TestUsers map[string]*providers.ExternalIdentity TestCredentials map[string]string // username -> password } @@ -124,11 +124,11 @@ func (m *MockLDAPProvider) ValidateToken(ctx context.Context, token string) (*pr return &providers.TokenClaims{ Subject: username, Claims: map[string]interface{}{ - "ldap_dn": "CN=" + username + ",DC=test,DC=com", - "email": identity.Email, - "name": identity.DisplayName, - "groups": identity.Groups, - "provider": m.name, + "ldap_dn": "CN=" + username + ",DC=test,DC=com", + "email": identity.Email, + "name": identity.DisplayName, + "groups": identity.Groups, + "provider": m.name, }, }, nil } diff --git a/weed/iam/oidc/oidc_provider.go b/weed/iam/oidc/oidc_provider.go index e526772d7..95468812f 100644 --- a/weed/iam/oidc/oidc_provider.go +++ b/weed/iam/oidc/oidc_provider.go @@ -59,7 +59,7 @@ func (p *OIDCProvider) Initialize(config interface{}) error { if config == nil { return fmt.Errorf("config cannot be nil") } - + oidcConfig, ok := config.(*OIDCConfig) if !ok { return fmt.Errorf("invalid config type for OIDC provider") @@ -114,7 +114,7 @@ func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*provide email, _ := claims.GetClaimString("email") displayName, _ := claims.GetClaimString("name") groups, _ := claims.GetClaimStringSlice("groups") - + return &providers.ExternalIdentity{ UserID: claims.Subject, Email: email, @@ -138,7 +138,7 @@ func (p *OIDCProvider) GetUserInfo(ctx context.Context, userID string) (*provide // 1. Make HTTP request to UserInfo endpoint // 2. Parse response and extract user claims // 3. Map claims to ExternalIdentity structure - + return nil, fmt.Errorf("UserInfo endpoint integration not implemented yet") } @@ -153,11 +153,11 @@ func (p *OIDCProvider) ValidateToken(ctx context.Context, token string) (*provid } // TODO: Implement actual JWT token validation - // 1. Parse JWT token + // 1. Parse JWT token // 2. Verify signature using JWKS from provider // 3. Validate claims (iss, aud, exp, etc.) // 4. Extract user claims - + return nil, fmt.Errorf("JWT validation not implemented yet - requires JWKS integration") } @@ -167,7 +167,7 @@ func (p *OIDCProvider) mapClaimsToRoles(claims *providers.TokenClaims) []string // Get groups from claims groups, _ := claims.GetClaimStringSlice("groups") - + // Basic role mapping based on groups for _, group := range groups { switch group { diff --git a/weed/iam/policy/policy_engine_test.go b/weed/iam/policy/policy_engine_test.go index c2901933d..fdece162f 100644 --- a/weed/iam/policy/policy_engine_test.go +++ b/weed/iam/policy/policy_engine_test.go @@ -41,9 +41,9 @@ func TestPolicyEngineInitialization(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine := NewPolicyEngine() - + err := engine.Initialize(tt.config) - + if tt.wantErr { assert.Error(t, err) } else { @@ -120,7 +120,7 @@ func TestPolicyDocumentValidation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePolicyDocument(tt.policy) - + if tt.wantErr { assert.Error(t, err) if tt.errorMsg != "" { @@ -136,26 +136,26 @@ func TestPolicyDocumentValidation(t *testing.T) { // TestPolicyEvaluation tests policy evaluation logic func TestPolicyEvaluation(t *testing.T) { engine := setupTestPolicyEngine(t) - + // Add test policies readPolicy := &PolicyDocument{ Version: "2012-10-17", Statement: []Statement{ { - Sid: "AllowS3Read", - Effect: "Allow", - Action: []string{"s3:GetObject", "s3:ListBucket"}, + Sid: "AllowS3Read", + Effect: "Allow", + Action: []string{"s3:GetObject", "s3:ListBucket"}, Resource: []string{ - "arn:seaweed:s3:::public-bucket/*", // For object operations - "arn:seaweed:s3:::public-bucket", // For bucket operations + "arn:seaweed:s3:::public-bucket/*", // For object operations + "arn:seaweed:s3:::public-bucket", // For bucket operations }, }, }, } - + err := engine.AddPolicy("read-policy", readPolicy) require.NoError(t, err) - + denyPolicy := &PolicyDocument{ Version: "2012-10-17", Statement: []Statement{ @@ -167,7 +167,7 @@ func TestPolicyEvaluation(t *testing.T) { }, }, } - + err = engine.AddPolicy("deny-policy", denyPolicy) require.NoError(t, err) @@ -225,10 +225,10 @@ func TestPolicyEvaluation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := engine.Evaluate(context.Background(), tt.context, tt.policies) - + assert.NoError(t, err) assert.Equal(t, tt.want, result.Effect) - + // Verify evaluation details assert.NotNil(t, result.EvaluationDetails) assert.Equal(t, tt.context.Action, result.EvaluationDetails.Action) @@ -240,7 +240,7 @@ func TestPolicyEvaluation(t *testing.T) { // TestConditionEvaluation tests policy conditions func TestConditionEvaluation(t *testing.T) { engine := setupTestPolicyEngine(t) - + // Policy with IP address condition conditionalPolicy := &PolicyDocument{ Version: "2012-10-17", @@ -258,7 +258,7 @@ func TestConditionEvaluation(t *testing.T) { }, }, } - + err := engine.AddPolicy("ip-conditional", conditionalPolicy) require.NoError(t, err) @@ -308,7 +308,7 @@ func TestConditionEvaluation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := engine.Evaluate(context.Background(), tt.context, []string{"ip-conditional"}) - + assert.NoError(t, err) assert.Equal(t, tt.want, result.Effect) }) @@ -318,10 +318,10 @@ func TestConditionEvaluation(t *testing.T) { // TestResourceMatching tests resource ARN matching func TestResourceMatching(t *testing.T) { tests := []struct { - name string - policyResource string + name string + policyResource string requestResource string - want bool + want bool }{ { name: "exact match", @@ -418,9 +418,9 @@ func setupTestPolicyEngine(t *testing.T) *PolicyEngine { DefaultEffect: "Deny", StoreType: "memory", } - + err := engine.Initialize(config) require.NoError(t, err) - + return engine } diff --git a/weed/iam/policy/policy_store.go b/weed/iam/policy/policy_store.go index 8979936cd..997aee326 100644 --- a/weed/iam/policy/policy_store.go +++ b/weed/iam/policy/policy_store.go @@ -24,14 +24,14 @@ func (s *MemoryPolicyStore) StorePolicy(ctx context.Context, name string, policy if name == "" { return fmt.Errorf("policy name cannot be empty") } - + if policy == nil { return fmt.Errorf("policy cannot be nil") } - + s.mutex.Lock() defer s.mutex.Unlock() - + // Deep copy the policy to prevent external modifications s.policies[name] = copyPolicyDocument(policy) return nil @@ -42,15 +42,15 @@ func (s *MemoryPolicyStore) GetPolicy(ctx context.Context, name string) (*Policy if name == "" { return nil, fmt.Errorf("policy name cannot be empty") } - + s.mutex.RLock() defer s.mutex.RUnlock() - + policy, exists := s.policies[name] if !exists { return nil, fmt.Errorf("policy not found: %s", name) } - + // Return a copy to prevent external modifications return copyPolicyDocument(policy), nil } @@ -60,10 +60,10 @@ func (s *MemoryPolicyStore) DeletePolicy(ctx context.Context, name string) error if name == "" { return fmt.Errorf("policy name cannot be empty") } - + s.mutex.Lock() defer s.mutex.Unlock() - + delete(s.policies, name) return nil } @@ -72,12 +72,12 @@ func (s *MemoryPolicyStore) DeletePolicy(ctx context.Context, name string) error func (s *MemoryPolicyStore) ListPolicies(ctx context.Context) ([]string, error) { s.mutex.RLock() defer s.mutex.RUnlock() - + names := make([]string, 0, len(s.policies)) for name := range s.policies { names = append(names, name) } - + return names, nil } @@ -86,12 +86,12 @@ func copyPolicyDocument(original *PolicyDocument) *PolicyDocument { if original == nil { return nil } - + copied := &PolicyDocument{ Version: original.Version, Id: original.Id, } - + // Copy statements copied.Statement = make([]Statement, len(original.Statement)) for i, stmt := range original.Statement { @@ -101,31 +101,31 @@ func copyPolicyDocument(original *PolicyDocument) *PolicyDocument { Principal: stmt.Principal, NotPrincipal: stmt.NotPrincipal, } - + // Copy action slice if stmt.Action != nil { copied.Statement[i].Action = make([]string, len(stmt.Action)) copy(copied.Statement[i].Action, stmt.Action) } - + // Copy NotAction slice if stmt.NotAction != nil { copied.Statement[i].NotAction = make([]string, len(stmt.NotAction)) copy(copied.Statement[i].NotAction, stmt.NotAction) } - + // Copy resource slice if stmt.Resource != nil { copied.Statement[i].Resource = make([]string, len(stmt.Resource)) copy(copied.Statement[i].Resource, stmt.Resource) } - + // Copy NotResource slice if stmt.NotResource != nil { copied.Statement[i].NotResource = make([]string, len(stmt.NotResource)) copy(copied.Statement[i].NotResource, stmt.NotResource) } - + // Copy condition map (shallow copy for now) if stmt.Condition != nil { copied.Statement[i].Condition = make(map[string]map[string]interface{}) @@ -134,7 +134,7 @@ func copyPolicyDocument(original *PolicyDocument) *PolicyDocument { } } } - + return copied } @@ -150,7 +150,7 @@ func NewFilerPolicyStore(config map[string]interface{}) (*FilerPolicyStore, erro // 1. Parse configuration for filer connection details // 2. Set up filer client // 3. Configure base path for policy storage - + return nil, fmt.Errorf("filer policy store not implemented yet") } @@ -160,7 +160,7 @@ func (s *FilerPolicyStore) StorePolicy(ctx context.Context, name string, policy // 1. Serialize policy to JSON // 2. Store in filer at basePath/policies/name.json // 3. Handle errors and retries - + return fmt.Errorf("filer policy storage not implemented yet") } @@ -170,7 +170,7 @@ func (s *FilerPolicyStore) GetPolicy(ctx context.Context, name string) (*PolicyD // 1. Read policy file from filer // 2. Deserialize JSON to PolicyDocument // 3. Handle not found cases - + return nil, fmt.Errorf("filer policy retrieval not implemented yet") } @@ -179,7 +179,7 @@ func (s *FilerPolicyStore) DeletePolicy(ctx context.Context, name string) error // TODO: Implement filer policy deletion // 1. Delete policy file from filer // 2. Handle errors - + return fmt.Errorf("filer policy deletion not implemented yet") } @@ -189,6 +189,6 @@ func (s *FilerPolicyStore) ListPolicies(ctx context.Context) ([]string, error) { // 1. List files in basePath/policies/ // 2. Extract policy names from filenames // 3. Return sorted list - + return nil, fmt.Errorf("filer policy listing not implemented yet") } diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index 66d050c34..036de499f 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -243,7 +243,7 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass if !s.initialized { return nil, fmt.Errorf("STS service not initialized") } - + if request == nil { return nil, fmt.Errorf("request cannot be nil") } @@ -300,8 +300,8 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass // 7. Build and return response assumedRoleUser := &AssumedRoleUser{ AssumedRoleId: request.RoleArn, - Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), - Subject: externalIdentity.UserID, + Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), + Subject: externalIdentity.UserID, } return &AssumeRoleResponse{ @@ -315,7 +315,7 @@ func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, request *Ass if !s.initialized { return nil, fmt.Errorf("STS service not initialized") } - + if request == nil { return nil, fmt.Errorf("request cannot be nil") } @@ -379,8 +379,8 @@ func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, request *Ass // 8. Build and return response assumedRoleUser := &AssumedRoleUser{ AssumedRoleId: request.RoleArn, - Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), - Subject: externalIdentity.UserID, + Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), + Subject: externalIdentity.UserID, } return &AssumeRoleResponse{ @@ -394,34 +394,34 @@ func (s *STSService) ValidateSessionToken(ctx context.Context, sessionToken stri if !s.initialized { return nil, fmt.Errorf("STS service not initialized") } - + if sessionToken == "" { return nil, fmt.Errorf("session token cannot be empty") } - + // For now, use the session token as session ID directly // In a full implementation, this would: // 1. Parse JWT session token // 2. Verify signature and expiration // 3. Extract session ID from claims - + // Extract session ID (simplified - assuming token contains session ID directly) sessionId := s.extractSessionIdFromToken(sessionToken) if sessionId == "" { return nil, fmt.Errorf("invalid session token format") } - + // Retrieve session from store session, err := s.sessionStore.GetSession(ctx, sessionId) if err != nil { return nil, fmt.Errorf("session validation failed: %w", err) } - + // Additional validation can be added here if session.ExpiresAt.Before(time.Now()) { return nil, fmt.Errorf("session has expired") } - + return session, nil } @@ -430,23 +430,23 @@ func (s *STSService) RevokeSession(ctx context.Context, sessionToken string) err if !s.initialized { return fmt.Errorf("STS service not initialized") } - + if sessionToken == "" { return fmt.Errorf("session token cannot be empty") } - + // Extract session ID from token sessionId := s.extractSessionIdFromToken(sessionToken) if sessionId == "" { return fmt.Errorf("invalid session token format") } - + // Remove session from store err := s.sessionStore.RevokeSession(ctx, sessionId) if err != nil { return fmt.Errorf("failed to revoke session: %w", err) } - + return nil } @@ -457,22 +457,22 @@ func (s *STSService) validateAssumeRoleWithWebIdentityRequest(request *AssumeRol if request.RoleArn == "" { return fmt.Errorf("RoleArn is required") } - + if request.WebIdentityToken == "" { return fmt.Errorf("WebIdentityToken is required") } - + if request.RoleSessionName == "" { return fmt.Errorf("RoleSessionName is required") } - + // Validate session duration if provided if request.DurationSeconds != nil { if *request.DurationSeconds < 900 || *request.DurationSeconds > 43200 { // 15min to 12 hours return fmt.Errorf("DurationSeconds must be between 900 and 43200 seconds") } } - + return nil } @@ -486,7 +486,7 @@ func (s *STSService) validateWebIdentityToken(ctx context.Context, token string) return identity, provider, nil } } - + return nil, nil, fmt.Errorf("web identity token validation failed with all providers") } @@ -497,27 +497,27 @@ func (s *STSService) validateRoleAssumption(roleArn string, identity *providers. // 1. Role exists // 2. Role trust policy allows assumption by this identity // 3. Identity has permission to assume the role - + if roleArn == "" { return fmt.Errorf("role ARN cannot be empty") } - + if identity == nil { return fmt.Errorf("identity cannot be nil") } - + // Basic role ARN format validation expectedPrefix := "arn:seaweed:iam::role/" if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix { return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix) } - + // For testing, reject non-existent roles roleName := extractRoleNameFromArn(roleArn) if roleName == "NonExistentRole" { return fmt.Errorf("role does not exist: %s", roleName) } - + return nil } @@ -526,7 +526,7 @@ func (s *STSService) calculateSessionDuration(durationSeconds *int64) time.Durat if durationSeconds != nil { return time.Duration(*durationSeconds) * time.Second } - + // Use default from config return s.config.TokenDuration } @@ -536,7 +536,7 @@ func (s *STSService) extractSessionIdFromToken(sessionToken string) string { // For simplified implementation, we need to map session tokens to session IDs // The session token is stored as part of the credentials in the session // So we need to search through sessions to find the matching token - + // For now, use the session token directly as session ID since we store them together // In a full implementation, this would parse JWT and extract session ID from claims if len(sessionToken) > 10 && sessionToken[:2] == "ST" { @@ -544,12 +544,12 @@ func (s *STSService) extractSessionIdFromToken(sessionToken string) string { // This is inefficient but works for testing return s.findSessionIdByToken(sessionToken) } - + // For test compatibility, also handle direct session IDs if len(sessionToken) == 32 { // Typical session ID length return sessionToken } - + return "" } @@ -558,22 +558,22 @@ func (s *STSService) findSessionIdByToken(sessionToken string) string { // In a real implementation, we'd maintain a reverse index // For testing, we can use the fact that our memory store can be searched // This is a simplified approach - in production we'd use proper token->session mapping - + memStore, ok := s.sessionStore.(*MemorySessionStore) if !ok { return "" } - + // Search through all sessions to find matching token memStore.mutex.RLock() defer memStore.mutex.RUnlock() - + for sessionId, session := range memStore.sessions { if session.Credentials != nil && session.Credentials.SessionToken == sessionToken { return sessionId } } - + return "" } @@ -582,29 +582,29 @@ func (s *STSService) validateAssumeRoleWithCredentialsRequest(request *AssumeRol if request.RoleArn == "" { return fmt.Errorf("RoleArn is required") } - + if request.Username == "" { return fmt.Errorf("Username is required") } - + if request.Password == "" { return fmt.Errorf("Password is required") } - + if request.RoleSessionName == "" { return fmt.Errorf("RoleSessionName is required") } - + if request.ProviderName == "" { return fmt.Errorf("ProviderName is required") } - + // Validate session duration if provided if request.DurationSeconds != nil { if *request.DurationSeconds < 900 || *request.DurationSeconds > 43200 { // 15min to 12 hours return fmt.Errorf("DurationSeconds must be between 900 and 43200 seconds") } } - + return nil } diff --git a/weed/iam/sts/sts_service_test.go b/weed/iam/sts/sts_service_test.go index 8ab1be112..027080845 100644 --- a/weed/iam/sts/sts_service_test.go +++ b/weed/iam/sts/sts_service_test.go @@ -364,7 +364,7 @@ func (m *MockIdentityProvider) Authenticate(ctx context.Context, token string) ( if claims, exists := m.validTokens[token]; exists { email, _ := claims.GetClaimString("email") name, _ := claims.GetClaimString("name") - + return &providers.ExternalIdentity{ UserID: claims.Subject, Email: email, @@ -372,7 +372,7 @@ func (m *MockIdentityProvider) Authenticate(ctx context.Context, token string) ( Provider: m.name, }, nil } - + // Handle LDAP credentials (username:password format) if m.validCredentials != nil { parts := strings.Split(token, ":") @@ -388,7 +388,7 @@ func (m *MockIdentityProvider) Authenticate(ctx context.Context, token string) ( } } } - + return nil, fmt.Errorf("invalid token") }