Files
casdoor/object/syncer_awsiam.go
2026-02-11 01:00:41 +08:00

328 lines
8.8 KiB
Go

// 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
}