// Copyright 2026 The Casdoor Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package object import ( "context" "fmt" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/casdoor/casdoor/util" ) // AwsIamSyncerProvider implements SyncerProvider for AWS IAM API-based syncers type AwsIamSyncerProvider struct { Syncer *Syncer iamClient *iam.IAM } // InitAdapter initializes the AWS IAM syncer func (p *AwsIamSyncerProvider) InitAdapter() error { // syncer.Host should be the AWS region (e.g., "us-east-1") // syncer.User should be the AWS Access Key ID // syncer.Password should be the AWS Secret Access Key region := p.Syncer.Host if region == "" { return fmt.Errorf("AWS region (host field) is required for AWS IAM syncer") } accessKeyId := p.Syncer.User if accessKeyId == "" { return fmt.Errorf("AWS Access Key ID (user field) is required for AWS IAM syncer") } secretAccessKey := p.Syncer.Password if secretAccessKey == "" { return fmt.Errorf("AWS Secret Access Key (password field) is required for AWS IAM syncer") } // Create AWS session sess, err := session.NewSession(&aws.Config{ Region: aws.String(region), Credentials: credentials.NewStaticCredentials(accessKeyId, secretAccessKey, ""), }) if err != nil { return fmt.Errorf("failed to create AWS session: %w", err) } // Create IAM client p.iamClient = iam.New(sess) return nil } // GetOriginalUsers retrieves all users from AWS IAM API func (p *AwsIamSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) { if p.iamClient == nil { if err := p.InitAdapter(); err != nil { return nil, err } } return p.getAwsIamUsers() } // AddUser adds a new user to AWS IAM (not supported for read-only API) func (p *AwsIamSyncerProvider) AddUser(user *OriginalUser) (bool, error) { // AWS IAM syncer is typically read-only return false, fmt.Errorf("adding users to AWS IAM is not supported") } // UpdateUser updates an existing user in AWS IAM (not supported for read-only API) func (p *AwsIamSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) { // AWS IAM syncer is typically read-only return false, fmt.Errorf("updating users in AWS IAM is not supported") } // TestConnection tests the AWS IAM API connection func (p *AwsIamSyncerProvider) TestConnection() error { if p.iamClient == nil { if err := p.InitAdapter(); err != nil { return err } } // Try to list users with a limit of 1 to test the connection ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() input := &iam.ListUsersInput{ MaxItems: aws.Int64(1), } _, err := p.iamClient.ListUsersWithContext(ctx, input) return err } // Close closes any open connections func (p *AwsIamSyncerProvider) Close() error { // AWS IAM client doesn't require explicit cleanup p.iamClient = nil return nil } // getAwsIamUsers gets all users from AWS IAM API func (p *AwsIamSyncerProvider) getAwsIamUsers() ([]*OriginalUser, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() allUsers := []*iam.User{} var marker *string // Paginate through all users for { input := &iam.ListUsersInput{ Marker: marker, } result, err := p.iamClient.ListUsersWithContext(ctx, input) if err != nil { return nil, fmt.Errorf("failed to list IAM users: %w", err) } allUsers = append(allUsers, result.Users...) if result.IsTruncated == nil || !*result.IsTruncated { break } marker = result.Marker } // Convert AWS IAM users to Casdoor OriginalUser originalUsers := []*OriginalUser{} for _, iamUser := range allUsers { originalUser, err := p.awsIamUserToOriginalUser(iamUser) if err != nil { // Log error but continue processing other users userName := "unknown" if iamUser.UserName != nil { userName = *iamUser.UserName } fmt.Printf("Warning: Failed to convert IAM user %s: %v\n", userName, err) continue } originalUsers = append(originalUsers, originalUser) } return originalUsers, nil } // awsIamUserToOriginalUser converts AWS IAM user to Casdoor OriginalUser func (p *AwsIamSyncerProvider) awsIamUserToOriginalUser(iamUser *iam.User) (*OriginalUser, error) { if iamUser == nil { return nil, fmt.Errorf("IAM user is nil") } user := &OriginalUser{ Address: []string{}, Properties: map[string]string{}, Groups: []string{}, } // Set ID from UserId (unique identifier) if iamUser.UserId != nil { user.Id = *iamUser.UserId } // Set Name from UserName if iamUser.UserName != nil { user.Name = *iamUser.UserName } // Set DisplayName (use UserName if not available separately) if iamUser.UserName != nil { user.DisplayName = *iamUser.UserName } // Set CreatedTime from CreateDate if iamUser.CreateDate != nil { user.CreatedTime = iamUser.CreateDate.Format(time.RFC3339) } else { user.CreatedTime = util.GetCurrentTime() } // Get user tags which might contain additional information ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() tagsInput := &iam.ListUserTagsInput{ UserName: iamUser.UserName, } tagsResult, err := p.iamClient.ListUserTagsWithContext(ctx, tagsInput) if err == nil && tagsResult != nil { // Process tags to extract additional user information for _, tag := range tagsResult.Tags { if tag.Key != nil && tag.Value != nil { key := *tag.Key value := *tag.Value switch key { case "Email", "email": user.Email = value case "Phone", "phone": user.Phone = value case "DisplayName", "displayName": user.DisplayName = value case "FirstName", "firstName": user.FirstName = value case "LastName", "lastName": user.LastName = value case "Title", "title": user.Title = value case "Department", "department": user.Affiliation = value default: // Store other tags in Properties user.Properties[key] = value } } } } // AWS IAM users are active by default unless specified in tags // Check if there's a "Status" or "Active" tag if status, ok := user.Properties["Status"]; ok { if status == "Inactive" || status == "Disabled" { user.IsForbidden = true } } if active, ok := user.Properties["Active"]; ok { if active == "false" || active == "False" || active == "0" { user.IsForbidden = true } } return user, nil } // GetOriginalGroups retrieves all groups from AWS IAM func (p *AwsIamSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) { if p.iamClient == nil { if err := p.InitAdapter(); err != nil { return nil, err } } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() allGroups := []*iam.Group{} var marker *string // Paginate through all groups for { input := &iam.ListGroupsInput{ Marker: marker, } result, err := p.iamClient.ListGroupsWithContext(ctx, input) if err != nil { return nil, fmt.Errorf("failed to list IAM groups: %w", err) } allGroups = append(allGroups, result.Groups...) if result.IsTruncated == nil || !*result.IsTruncated { break } marker = result.Marker } // Convert AWS IAM groups to Casdoor OriginalGroup originalGroups := []*OriginalGroup{} for _, iamGroup := range allGroups { if iamGroup.GroupId != nil && iamGroup.GroupName != nil { group := &OriginalGroup{ Id: *iamGroup.GroupId, Name: *iamGroup.GroupName, } if iamGroup.GroupName != nil { group.DisplayName = *iamGroup.GroupName } originalGroups = append(originalGroups, group) } } return originalGroups, nil } // GetOriginalUserGroups retrieves the group IDs that a user belongs to func (p *AwsIamSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) { if p.iamClient == nil { if err := p.InitAdapter(); err != nil { return nil, err } } // Note: AWS IAM API requires UserName to query groups, but this interface provides UserId. // This is a known limitation. To properly implement this, we would need to: // 1. Maintain a mapping cache from UserId to UserName, or // 2. Modify the interface to accept both UserId and UserName // For now, returning empty groups to maintain interface compatibility. // TODO: Implement user group synchronization by maintaining a UserId->UserName mapping return []string{}, nil }