package s3api

// This file provides IAM API functionality embedded in the S3 server.
// Common IAM types and helpers are imported from the shared weed/iam package.

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/seaweedfs/seaweedfs/weed/credential"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	iamlib "github.com/seaweedfs/seaweedfs/weed/iam"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	"github.com/seaweedfs/seaweedfs/weed/util/request_id"
	"google.golang.org/protobuf/proto"
)

// EmbeddedIamApi provides IAM API functionality embedded in the S3 server.
// This allows running a single server that handles both S3 and IAM requests.
type EmbeddedIamApi struct {
	credentialManager *credential.CredentialManager
	iam               *IdentityAccessManagement
	policyLock        sync.RWMutex
	// Test hook
	getS3ApiConfigurationFunc func(*iam_pb.S3ApiConfiguration) error
	putS3ApiConfigurationFunc func(*iam_pb.S3ApiConfiguration) error
	reloadConfigurationFunc   func() error
	readOnly                  bool
}

// NewEmbeddedIamApi creates a new embedded IAM API handler.
func NewEmbeddedIamApi(credentialManager *credential.CredentialManager, iam *IdentityAccessManagement, readOnly bool) *EmbeddedIamApi {
	return &EmbeddedIamApi{
		credentialManager: credentialManager,
		iam:               iam,
		readOnly:          readOnly,
	}
}

func (e *EmbeddedIamApi) refreshIAMConfiguration() error {
	if e.reloadConfigurationFunc != nil {
		return e.reloadConfigurationFunc()
	}
	if e.iam == nil {
		return nil
	}
	if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
		return fmt.Errorf("failed to refresh IAM configuration: %w", err)
	}
	return nil
}

// Constants for service account identifiers
const (
	ServiceAccountIDLength  = 12 // Length of the service account ID
	AccessKeyLength         = 20 // AWS standard access key length
	SecretKeyLength         = 40 // AWS standard secret key length (base64 encoded)
	ServiceAccountIDPrefix  = "sa"
	ServiceAccountKeyPrefix = "ABIA" // Service account access keys start with ABIA
	UserAccessKeyPrefix     = "AKIA" // User access keys start with AKIA

	// Operational limits (AWS IAM compatible)
	MaxServiceAccountsPerUser = 100  // Maximum service accounts per user
	MaxDescriptionLength      = 1000 // Maximum description length in characters
	MaxManagedPoliciesPerUser = 10   // Maximum managed policies attached to a user
	MaxUserTags               = 50   // Maximum tags per user
	MaxUserTagKeyLength       = 128  // Maximum tag key length
	MaxUserTagValueLength     = 256  // Maximum tag value length
)

// Type aliases for IAM response types from shared package
type (
	iamListUsersResponse                = iamlib.ListUsersResponse
	iamListAccessKeysResponse           = iamlib.ListAccessKeysResponse
	iamDeleteAccessKeyResponse          = iamlib.DeleteAccessKeyResponse
	iamCreatePolicyResponse             = iamlib.CreatePolicyResponse
	iamDeletePolicyResponse             = iamlib.DeletePolicyResponse
	iamListPoliciesResponse             = iamlib.ListPoliciesResponse
	iamGetPolicyResponse                = iamlib.GetPolicyResponse
	iamListPolicyVersionsResponse       = iamlib.ListPolicyVersionsResponse
	iamGetPolicyVersionResponse         = iamlib.GetPolicyVersionResponse
	iamCreatePolicyVersionResponse      = iamlib.CreatePolicyVersionResponse
	iamDeletePolicyVersionResponse      = iamlib.DeletePolicyVersionResponse
	iamCreateUserResponse               = iamlib.CreateUserResponse
	iamDeleteUserResponse               = iamlib.DeleteUserResponse
	iamGetUserResponse                  = iamlib.GetUserResponse
	iamUpdateUserResponse               = iamlib.UpdateUserResponse
	iamCreateAccessKeyResponse          = iamlib.CreateAccessKeyResponse
	iamPutUserPolicyResponse            = iamlib.PutUserPolicyResponse
	iamDeleteUserPolicyResponse         = iamlib.DeleteUserPolicyResponse
	iamGetUserPolicyResponse            = iamlib.GetUserPolicyResponse
	iamListUserPoliciesResponse         = iamlib.ListUserPoliciesResponse
	iamAttachUserPolicyResponse         = iamlib.AttachUserPolicyResponse
	iamDetachUserPolicyResponse         = iamlib.DetachUserPolicyResponse
	iamListAttachedUserPoliciesResponse = iamlib.ListAttachedUserPoliciesResponse
	iamSetUserStatusResponse            = iamlib.SetUserStatusResponse
	iamUpdateAccessKeyResponse          = iamlib.UpdateAccessKeyResponse
	iamErrorResponse                    = iamlib.ErrorResponse
	iamError                            = iamlib.Error
	// Service account response types
	iamServiceAccountInfo           = iamlib.ServiceAccountInfo
	iamCreateServiceAccountResponse = iamlib.CreateServiceAccountResponse
	iamDeleteServiceAccountResponse = iamlib.DeleteServiceAccountResponse
	iamListServiceAccountsResponse  = iamlib.ListServiceAccountsResponse
	iamGetServiceAccountResponse    = iamlib.GetServiceAccountResponse
	iamUpdateServiceAccountResponse = iamlib.UpdateServiceAccountResponse
	// Group response types
	iamCreateGroupResponse               = iamlib.CreateGroupResponse
	iamDeleteGroupResponse               = iamlib.DeleteGroupResponse
	iamUpdateGroupResponse               = iamlib.UpdateGroupResponse
	iamGetGroupResponse                  = iamlib.GetGroupResponse
	iamListGroupsResponse                = iamlib.ListGroupsResponse
	iamAddUserToGroupResponse            = iamlib.AddUserToGroupResponse
	iamRemoveUserFromGroupResponse       = iamlib.RemoveUserFromGroupResponse
	iamAttachGroupPolicyResponse         = iamlib.AttachGroupPolicyResponse
	iamDetachGroupPolicyResponse         = iamlib.DetachGroupPolicyResponse
	iamListAttachedGroupPoliciesResponse = iamlib.ListAttachedGroupPoliciesResponse
	iamPutGroupPolicyResponse            = iamlib.PutGroupPolicyResponse
	iamGetGroupPolicyResponse            = iamlib.GetGroupPolicyResponse
	iamDeleteGroupPolicyResponse         = iamlib.DeleteGroupPolicyResponse
	iamListGroupPoliciesResponse         = iamlib.ListGroupPoliciesResponse
	iamListGroupsForUserResponse         = iamlib.ListGroupsForUserResponse
	// User tag response types
	iamTagUserResponse      = iamlib.TagUserResponse
	iamUntagUserResponse    = iamlib.UntagUserResponse
	iamListUserTagsResponse = iamlib.ListUserTagsResponse
	iamTag                  = iamlib.IAMTag
)

// Helper function wrappers using shared package
func iamHash(s *string) string {
	return iamlib.Hash(s)
}

func iamStringWithCharset(length int, charset string) (string, error) {
	return iamlib.GenerateRandomString(length, charset)
}

func iamStringSlicesEqual(a, b []string) bool {
	return iamlib.StringSlicesEqual(a, b)
}

func iamMapToStatementAction(action string) string {
	return iamlib.MapToStatementAction(action)
}

func iamMapToIdentitiesAction(action string) string {
	return iamlib.MapToIdentitiesAction(action)
}

// iamValidateStatus validates that status is either Active or Inactive.
func iamValidateStatus(status string) error {
	switch status {
	case iamAccessKeyStatusActive, iamAccessKeyStatusInactive:
		return nil
	case "":
		return fmt.Errorf("Status parameter is required")
	default:
		return fmt.Errorf("Status must be '%s' or '%s'", iamAccessKeyStatusActive, iamAccessKeyStatusInactive)
	}
}

// Constants from shared package
const (
	iamCharsetUpper            = iamlib.CharsetUpper
	iamCharset                 = iamlib.Charset
	iamPolicyDocumentVersion   = iamlib.PolicyDocumentVersion
	iamUserDoesNotExist        = iamlib.UserDoesNotExist
	iamAccessKeyStatusActive   = iamlib.AccessKeyStatusActive
	iamAccessKeyStatusInactive = iamlib.AccessKeyStatusInactive
)

func newIamErrorResponse(errCode string, errMsg string, requestID string) iamErrorResponse {
	errorResp := iamErrorResponse{}
	errorResp.Error.Type = "Sender"
	errorResp.Error.Code = &errCode
	errorResp.Error.Message = &errMsg
	errorResp.SetRequestId(requestID)
	return errorResp
}

func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Request, reqID string, iamErr *iamError) {
	if iamErr == nil {
		glog.Errorf("writeIamErrorResponse called with nil error")
		internalResp := newIamErrorResponse(iam.ErrCodeServiceFailureException, "Internal server error", reqID)
		s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalResp)
		return
	}

	errCode := iamErr.Code
	errMsg := iamErr.Error.Error()
	glog.Errorf("IAM Response %+v", errMsg)

	errorResp := newIamErrorResponse(errCode, errMsg, reqID)
	internalErrorResponse := newIamErrorResponse(iam.ErrCodeServiceFailureException, "Internal server error", reqID)

	switch errCode {
	case iam.ErrCodeNoSuchEntityException:
		s3err.WriteXMLResponse(w, r, http.StatusNotFound, errorResp)
	case iam.ErrCodeEntityAlreadyExistsException:
		s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp)
	case iam.ErrCodeMalformedPolicyDocumentException, iam.ErrCodeInvalidInputException, "ValidationError":
		s3err.WriteXMLResponse(w, r, http.StatusBadRequest, errorResp)
	case "AccessDenied", iam.ErrCodeLimitExceededException:
		s3err.WriteXMLResponse(w, r, http.StatusForbidden, errorResp)
	case iam.ErrCodeServiceFailureException:
		s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
	case "NotImplemented":
		s3err.WriteXMLResponse(w, r, http.StatusNotImplemented, errorResp)
	case iam.ErrCodeDeleteConflictException:
		s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp)
	default:
		s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
	}
}

// GetS3ApiConfiguration loads the S3 API configuration from the credential manager.
// The credential manager automatically includes static identities in the result.
func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
	if e.getS3ApiConfigurationFunc != nil {
		return e.getS3ApiConfigurationFunc(s3cfg)
	}
	config, err := e.credentialManager.LoadConfiguration(context.Background())
	if err != nil {
		return fmt.Errorf("failed to load configuration: %w", err)
	}
	proto.Merge(s3cfg, config)
	return nil
}

// PutS3ApiConfiguration saves the S3 API configuration to the credential manager.
// The credential manager automatically filters out static identities before saving.
func (e *EmbeddedIamApi) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
	if e.putS3ApiConfigurationFunc != nil {
		return e.putS3ApiConfigurationFunc(s3cfg)
	}
	return e.credentialManager.SaveConfiguration(context.Background(), s3cfg)
}

// ReloadConfiguration reloads the IAM configuration from the credential manager.
func (e *EmbeddedIamApi) ReloadConfiguration() error {
	glog.V(4).Infof("IAM: reloading configuration via EmbeddedIamApi")
	if e.reloadConfigurationFunc != nil {
		return e.reloadConfigurationFunc()
	}
	return e.iam.LoadS3ApiConfigurationFromCredentialManager()
}

// ListUsers lists all IAM users.
func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamListUsersResponse {
	resp := &iamListUsersResponse{}
	for _, ident := range s3cfg.Identities {
		user := iamlib.NewUser(ident.Name)
		resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &user)
	}
	return resp
}

// ListAccessKeys lists access keys for a user.
func (e *EmbeddedIamApi) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamListAccessKeysResponse {
	resp := &iamListAccessKeysResponse{}
	userName := values.Get("UserName")
	for _, ident := range s3cfg.Identities {
		if userName != "" && userName != ident.Name {
			continue
		}
		for _, cred := range ident.Credentials {
			// Return actual status from credential, default to Active if not set
			status := cred.Status
			if status == "" {
				status = iamAccessKeyStatusActive
			}
			// Capture copies to avoid loop variable pointer aliasing
			identName := ident.Name
			accessKey := cred.AccessKey
			statusCopy := status
			resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata,
				&iam.AccessKeyMetadata{UserName: &identName, AccessKeyId: &accessKey, Status: &statusCopy},
			)
		}
	}
	return resp
}

// CreateUser creates a new IAM user.
func (e *EmbeddedIamApi) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamCreateUserResponse, *iamError) {
	resp := &iamCreateUserResponse{}
	userName := values.Get("UserName")

	// Validate UserName is not empty
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}

	// Check for duplicate user
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("user %s already exists", userName)}
		}
	}

	resp.CreateUserResult.User = iamlib.NewUser(userName)
	s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) // Disabled defaults to false (enabled)
	return resp, nil
}

// DeleteUser deletes an IAM user.
func (e *EmbeddedIamApi) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (*iamDeleteUserResponse, *iamError) {
	resp := &iamDeleteUserResponse{}
	for i, ident := range s3cfg.Identities {
		if userName == ident.Name {
			// AWS IAM behavior: prevent deletion if user has service accounts
			// This ensures explicit cleanup and prevents orphaned resources
			if len(ident.ServiceAccountIds) > 0 {
				return resp, &iamError{
					Code: iam.ErrCodeDeleteConflictException,
					Error: fmt.Errorf("cannot delete user %s: user has %d service account(s). Delete service accounts first",
						userName, len(ident.ServiceAccountIds)),
				}
			}
			s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...)
			// Remove user from all groups
			for _, g := range s3cfg.Groups {
				for j, m := range g.Members {
					if m == userName {
						g.Members = append(g.Members[:j], g.Members[j+1:]...)
						break
					}
				}
			}
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// GetUser gets an IAM user.
func (e *EmbeddedIamApi) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (*iamGetUserResponse, *iamError) {
	resp := &iamGetUserResponse{}
	for _, ident := range s3cfg.Identities {
		if userName == ident.Name {
			resp.GetUserResult.User = iamlib.NewUser(ident.Name)
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// UpdateUser updates an IAM user.
func (e *EmbeddedIamApi) UpdateUser(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUpdateUserResponse, *iamError) {
	resp := &iamUpdateUserResponse{}
	userName := values.Get("UserName")
	newUserName := values.Get("NewUserName")
	if newUserName == "" {
		return resp, nil
	}

	// Find the source identity first
	var sourceIdent *iam_pb.Identity
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			sourceIdent = ident
			break
		}
	}
	if sourceIdent == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
	}

	// No-op if renaming to the same name
	if newUserName == userName {
		return resp, nil
	}

	// Check for name collision before renaming
	for _, ident := range s3cfg.Identities {
		if ident.Name == newUserName {
			return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("user %s already exists", newUserName)}
		}
	}

	// Migrate the credential store's view of this user before the rename
	// takes effect. Stores with referential integrity (Postgres) MUST
	// implement UserRenamer: per-row Put / Get / Delete migration would
	// violate the FK on user_inline_policies(username) at statement time
	// because the new users row does not exist yet. UserRenamer does the
	// rename, credential re-pointing and inline-policy re-pointing inside
	// a single transaction.
	//
	// For stores that don't have FK enforcement (memory, filer_etc) the
	// renamer is also implemented as a plain rebinding so the regression
	// test exercises the same code path.
	if e.credentialManager != nil {
		switch supported, err := e.credentialManager.RenameUser(ctx, userName, newUserName); {
		case err != nil:
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("rename user %s -> %s: %w", userName, newUserName, err)}
		case !supported:
			// Fallback: copy the inline policies under the new name and
			// let the subsequent SaveConfiguration prune CASCADE the old
			// rows. Only safe for stores without FK enforcement.
			policyNames, err := e.credentialManager.ListUserInlinePolicies(ctx, userName)
			if err != nil {
				return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("list inline policies for %s: %w", userName, err)}
			}
			for _, policyName := range policyNames {
				doc, err := e.credentialManager.GetUserInlinePolicy(ctx, userName, policyName)
				if err != nil {
					return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("read inline policy %s for %s: %w", policyName, userName, err)}
				}
				if doc == nil {
					continue
				}
				if err := e.credentialManager.PutUserInlinePolicy(ctx, newUserName, policyName, *doc); err != nil {
					return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("copy inline policy %s to %s: %w", policyName, newUserName, err)}
				}
			}
		}
	}

	sourceIdent.Name = newUserName
	// Update group membership references
	for _, g := range s3cfg.Groups {
		for j, m := range g.Members {
			if m == userName {
				g.Members[j] = newUserName
				break
			}
		}
	}
	// Update service account parent references
	for _, sa := range s3cfg.ServiceAccounts {
		if sa.ParentUser == userName {
			sa.ParentUser = newUserName
		}
	}
	return resp, nil
}

// CreateAccessKey creates an access key for a user.
func (e *EmbeddedIamApi) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamCreateAccessKeyResponse, *iamError) {
	resp := &iamCreateAccessKeyResponse{}
	userName := values.Get("UserName")
	status := iam.StatusTypeActive

	accessKeyId := values.Get("AccessKeyId")
	secretAccessKey := values.Get("SecretAccessKey")
	if accessKeyId != "" {
		if err := iamlib.ValidateCallerSuppliedAccessKeyId(accessKeyId); err != nil {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
		}
	}
	if secretAccessKey != "" {
		if err := iamlib.ValidateCallerSuppliedSecretAccessKey(secretAccessKey); err != nil {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
		}
	}
	if (accessKeyId != "") != (secretAccessKey != "") {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId and SecretAccessKey must be supplied together")}
	}

	// Find the target user before touching the RNG or scanning for collisions,
	// so a missing user fails fast without consuming entropy.
	var target *iam_pb.Identity
	for _, ident := range s3cfg.Identities {
		if userName == ident.Name {
			target = ident
			break
		}
	}
	if target == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
	}

	if owner := iamlib.FindAccessKeyOwner(s3cfg, accessKeyId); owner != nil {
		glog.V(4).Infof("CreateAccessKey: supplied AccessKeyId already in use by %s %s", owner.Type, owner.Name)
		return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("AccessKeyId is already in use")}
	}
	if accessKeyId == "" {
		randomPart, err := iamStringWithCharset(AccessKeyLength-len(UserAccessKeyPrefix), iamCharsetUpper)
		if err != nil {
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)}
		}
		accessKeyId = UserAccessKeyPrefix + randomPart
	}
	if secretAccessKey == "" {
		var err error
		secretAccessKey, err = iamStringWithCharset(SecretKeyLength, iamCharset)
		if err != nil {
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate secret key: %w", err)}
		}
	}
	resp.CreateAccessKeyResult.AccessKey.AccessKeyId = &accessKeyId
	resp.CreateAccessKeyResult.AccessKey.SecretAccessKey = &secretAccessKey
	resp.CreateAccessKeyResult.AccessKey.UserName = &userName
	resp.CreateAccessKeyResult.AccessKey.Status = &status

	target.Credentials = append(target.Credentials,
		&iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey, Status: iamAccessKeyStatusActive})
	return resp, nil
}

// DeleteAccessKey deletes an access key for a user.
func (e *EmbeddedIamApi) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamDeleteAccessKeyResponse {
	resp := &iamDeleteAccessKeyResponse{}
	userName := values.Get("UserName")
	accessKeyId := values.Get("AccessKeyId")
	for _, ident := range s3cfg.Identities {
		if userName == ident.Name {
			for i, cred := range ident.Credentials {
				if cred.AccessKey == accessKeyId {
					ident.Credentials = append(ident.Credentials[:i], ident.Credentials[i+1:]...)
					break
				}
			}
			break
		}
	}
	return resp
}

// GetPolicyDocument parses a policy document string.
func (e *EmbeddedIamApi) GetPolicyDocument(policy *string) (policy_engine.PolicyDocument, error) {
	var policyDocument policy_engine.PolicyDocument
	if err := json.Unmarshal([]byte(*policy), &policyDocument); err != nil {
		return policy_engine.PolicyDocument{}, err
	}
	return policyDocument, nil
}

// CreatePolicy validates and creates a new IAM managed policy.
func (e *EmbeddedIamApi) CreatePolicy(ctx context.Context, values url.Values) (*iamCreatePolicyResponse, *iamError) {
	resp := &iamCreatePolicyResponse{}
	policyName := values.Get("PolicyName")
	policyDocumentString := values.Get("PolicyDocument")
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	if policyDocumentString == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	existing, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if existing != nil {
		return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("policy %s already exists", policyName)}
	}
	if err := e.credentialManager.CreatePolicy(ctx, policyName, policyDocument); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	policyId := iamHash(&policyName)
	arn := iamPolicyArn(policyName)
	resp.CreatePolicyResult.Policy.PolicyName = &policyName
	resp.CreatePolicyResult.Policy.Arn = &arn
	resp.CreatePolicyResult.Policy.PolicyId = &policyId
	path := "/"
	defaultVersionId := "v1"
	isAttachable := true
	resp.CreatePolicyResult.Policy.Path = &path
	resp.CreatePolicyResult.Policy.DefaultVersionId = &defaultVersionId
	resp.CreatePolicyResult.Policy.IsAttachable = &isAttachable
	return resp, nil
}

// DeletePolicy deletes a managed policy by ARN.
func (e *EmbeddedIamApi) DeletePolicy(ctx context.Context, values url.Values) (*iamDeletePolicyResponse, *iamError) {
	resp := &iamDeletePolicyResponse{}
	policyArn := values.Get("PolicyArn")
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}
	users, err := e.credentialManager.ListUsers(ctx)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	for _, user := range users {
		attachedPolicies, err := e.credentialManager.ListAttachedUserPolicies(ctx, user)
		if err != nil {
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
		}
		for _, attached := range attachedPolicies {
			if attached == policyName {
				return resp, &iamError{
					Code:  iam.ErrCodeDeleteConflictException,
					Error: fmt.Errorf("policy %s is attached to user %s", policyName, user),
				}
			}
		}
	}
	// Check if policy is attached to any group
	groupNames, err := e.credentialManager.ListGroups(ctx)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	for _, gn := range groupNames {
		g, err := e.credentialManager.GetGroup(ctx, gn)
		if err != nil {
			if errors.Is(err, credential.ErrGroupNotFound) {
				continue
			}
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to get group %s: %w", gn, err)}
		}
		for _, pn := range g.PolicyNames {
			if pn == policyName {
				return resp, &iamError{
					Code:  iam.ErrCodeDeleteConflictException,
					Error: fmt.Errorf("policy %s is attached to group %s", policyName, gn),
				}
			}
		}
	}
	if err := e.credentialManager.DeletePolicy(ctx, policyName); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	return resp, nil
}

// ListPolicies lists managed policies.
func (e *EmbeddedIamApi) ListPolicies(ctx context.Context, values url.Values) (*iamListPoliciesResponse, *iamError) {
	resp := &iamListPoliciesResponse{}
	pathPrefix := values.Get("PathPrefix")
	if pathPrefix == "" {
		pathPrefix = "/"
	}
	maxItems := 0
	if maxItemsStr := values.Get("MaxItems"); maxItemsStr != "" {
		parsedMaxItems, err := strconv.Atoi(maxItemsStr)
		if err != nil || parsedMaxItems <= 0 {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("MaxItems must be a positive integer")}
		}
		maxItems = parsedMaxItems
	}
	marker := values.Get("Marker")

	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}

	if pathPrefix != "/" {
		return resp, &iamError{Code: "NotImplemented", Error: fmt.Errorf("PathPrefix filtering is not supported yet")}
	}

	policyNames, err := e.credentialManager.ListPolicyNames(ctx)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	sort.Strings(policyNames)

	if marker != "" {
		i := sort.SearchStrings(policyNames, marker)
		if i < len(policyNames) && policyNames[i] == marker {
			policyNames = policyNames[i+1:]
		} else if i < len(policyNames) {
			policyNames = policyNames[i:]
		} else {
			policyNames = nil
		}
	}

	// Policy paths are not tracked in the current configuration, so PathPrefix filtering is not supported yet.
	for _, name := range policyNames {
		policyNameCopy := name
		policyArnCopy := iamPolicyArn(name)
		policyId := iamHash(&policyNameCopy)
		path := "/"
		defaultVersionId := "v1"
		isAttachable := true
		resp.ListPoliciesResult.Policies = append(resp.ListPoliciesResult.Policies, &iam.Policy{
			PolicyName:       &policyNameCopy,
			Arn:              &policyArnCopy,
			PolicyId:         &policyId,
			Path:             &path,
			DefaultVersionId: &defaultVersionId,
			IsAttachable:     &isAttachable,
		})
	}

	if maxItems > 0 && len(resp.ListPoliciesResult.Policies) > maxItems {
		resp.ListPoliciesResult.Policies = resp.ListPoliciesResult.Policies[:maxItems]
		resp.ListPoliciesResult.IsTruncated = true
		if name := resp.ListPoliciesResult.Policies[maxItems-1].PolicyName; name != nil {
			resp.ListPoliciesResult.Marker = *name
		}
		return resp, nil
	}

	resp.ListPoliciesResult.IsTruncated = false
	return resp, nil
}

// GetPolicy returns metadata for a managed policy.
func (e *EmbeddedIamApi) GetPolicy(ctx context.Context, values url.Values) (*iamGetPolicyResponse, *iamError) {
	resp := &iamGetPolicyResponse{}
	policyArn := values.Get("PolicyArn")
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}

	policyNameCopy := policyName
	policyArnCopy := iamPolicyArn(policyName)
	policyId := iamHash(&policyNameCopy)
	path := "/"
	defaultVersionId := "v1"
	isAttachable := true
	resp.GetPolicyResult.Policy = iam.Policy{
		PolicyName:       &policyNameCopy,
		Arn:              &policyArnCopy,
		PolicyId:         &policyId,
		Path:             &path,
		DefaultVersionId: &defaultVersionId,
		IsAttachable:     &isAttachable,
	}
	return resp, nil
}

// ListPolicyVersions lists versions for a managed policy.
// Current SeaweedFS implementation stores one version per policy (v1).
func (e *EmbeddedIamApi) ListPolicyVersions(ctx context.Context, values url.Values) (*iamListPolicyVersionsResponse, *iamError) {
	resp := &iamListPolicyVersionsResponse{}
	policyArn := values.Get("PolicyArn")
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}

	versionID := "v1"
	isDefaultVersion := true
	resp.ListPolicyVersionsResult.Versions = []*iam.PolicyVersion{{
		VersionId:        &versionID,
		IsDefaultVersion: &isDefaultVersion,
	}}
	resp.ListPolicyVersionsResult.IsTruncated = false
	return resp, nil
}

// GetPolicyVersion returns the document for a specific policy version.
// Current SeaweedFS implementation stores one version per policy (v1).
func (e *EmbeddedIamApi) GetPolicyVersion(ctx context.Context, values url.Values) (*iamGetPolicyVersionResponse, *iamError) {
	resp := &iamGetPolicyVersionResponse{}
	policyArn := values.Get("PolicyArn")
	versionID := values.Get("VersionId")
	if versionID == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
	}

	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}
	if versionID != "v1" {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionID)}
	}
	policyDocumentJSON, err := json.Marshal(policy)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	isDefaultVersion := true
	document := string(policyDocumentJSON)
	resp.GetPolicyVersionResult.PolicyVersion = iam.PolicyVersion{
		VersionId:        &versionID,
		IsDefaultVersion: &isDefaultVersion,
		Document:         &document,
	}
	return resp, nil
}

// CreatePolicyVersion replaces a managed policy's document with a new version.
// SeaweedFS keeps a single current document per managed policy (no version
// history), so the new document always becomes version "v1" / the default. This
// is what the AWS Terraform provider calls to update an aws_iam_policy in place.
func (e *EmbeddedIamApi) CreatePolicyVersion(ctx context.Context, values url.Values) (*iamCreatePolicyVersionResponse, *iamError) {
	resp := &iamCreatePolicyVersionResponse{}
	policyName, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	policyDocumentString := values.Get("PolicyDocument")
	if policyDocumentString == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")}
	}
	// SeaweedFS stores a single, always-default managed policy version. On AWS,
	// SetAsDefault=false stages a non-default version without activating it; we
	// can't honor that, so reject it rather than silently changing permissions.
	if !strings.EqualFold(values.Get("SetAsDefault"), "true") {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("SetAsDefault must be true: SeaweedFS stores a single managed policy version")}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	if _, err := e.getActions(&policyDocument); err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	existing, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if existing == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}
	if err := e.credentialManager.UpdatePolicy(ctx, policyName, policyDocument); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	versionID := "v1"
	isDefaultVersion := true
	document := policyDocumentString
	resp.CreatePolicyVersionResult.PolicyVersion = iam.PolicyVersion{
		VersionId:        &versionID,
		IsDefaultVersion: &isDefaultVersion,
		Document:         &document,
	}
	return resp, nil
}

// DeletePolicyVersion is accepted for API completeness. With a single stored
// version that is always the default, the only valid responses are NoSuchEntity
// (unknown version) and the AWS "cannot delete the default version" conflict.
func (e *EmbeddedIamApi) DeletePolicyVersion(ctx context.Context, values url.Values) (*iamDeletePolicyVersionResponse, *iamError) {
	resp := &iamDeletePolicyVersionResponse{}
	policyName, err := iamPolicyNameFromArn(values.Get("PolicyArn"))
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	versionID := values.Get("VersionId")
	if versionID == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}
	if versionID == "v1" {
		return resp, &iamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete the default version of policy %s", policyName)}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionID)}
}

func iamPolicyNameFromArn(policyArn string) (string, error) {
	const policyPathDelimiter = ":policy/"
	idx := strings.Index(policyArn, policyPathDelimiter)
	if idx < 0 {
		return "", fmt.Errorf("invalid policy arn: %s", policyArn)
	}

	policyPath := strings.Trim(policyArn[idx+len(policyPathDelimiter):], "/")
	if policyPath == "" {
		return "", fmt.Errorf("invalid policy arn: %s", policyArn)
	}

	parts := strings.Split(policyPath, "/")
	policyName := parts[len(parts)-1]
	if policyName == "" {
		return "", fmt.Errorf("invalid policy arn: %s", policyArn)
	}

	return policyName, nil
}

func iamPolicyArn(policyName string) string {
	return fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
}

// getActions extracts actions from a policy document.
// S3 ARN format: arn:aws:s3:::bucket or arn:aws:s3:::bucket/path/*
// res[5] contains the bucket and optional path after :::
func (e *EmbeddedIamApi) getActions(policy *policy_engine.PolicyDocument) ([]string, error) {
	var actions []string

	for _, statement := range policy.Statement {
		if statement.Effect != policy_engine.PolicyEffectAllow {
			return nil, fmt.Errorf("not a valid effect: '%s'. Only 'Allow' is possible", statement.Effect)
		}
		for _, resource := range statement.Resource.Strings() {
			// AWS IAM treats a bare "*" as "any resource". Normalize it to the
			// full-wildcard S3 ARN so it flows through the same path as
			// "arn:aws:s3:::*" instead of being dropped as malformed.
			var resourcePath string
			if resource == "*" {
				resourcePath = "*"
			} else {
				res := strings.Split(resource, ":")
				if len(res) != 6 || res[0] != "arn" || res[1] != "aws" || res[2] != "s3" {
					continue
				}
				resourcePath = res[5]
			}
			for _, action := range statement.Action.Strings() {
				act := strings.Split(action, ":")
				if len(act) != 2 || act[0] != "s3" {
					continue
				}
				statementAction := iamMapToStatementAction(act[1])
				if statementAction == "" {
					return nil, fmt.Errorf("not a valid action: '%s'", act[1])
				}

				if resourcePath == "*" {
					// Wildcard - applies to all buckets
					actions = append(actions, statementAction)
					continue
				}

				// Parse bucket and optional object path
				// Examples: "mybucket", "mybucket/*", "mybucket/prefix/*"
				bucket, objectPath, hasSep := strings.Cut(resourcePath, "/")
				if bucket == "" {
					continue // Invalid: empty bucket name
				}

				if !hasSep || objectPath == "" || objectPath == "*" {
					// Bucket-level or bucket/* - use just bucket name
					actions = append(actions, fmt.Sprintf("%s:%s", statementAction, bucket))
				} else {
					// Path-specific: preserve the object path, including any
					// trailing wildcard, so CanDo can match objects under the
					// prefix. Stripping the wildcard yielded a non-wildcard
					// action that only matched at bucket level.
					actions = append(actions, fmt.Sprintf("%s:%s/%s", statementAction, bucket, objectPath))
				}
			}
		}
	}

	if len(actions) == 0 {
		return nil, fmt.Errorf("no valid actions found in policy document")
	}
	return actions, nil
}

// recomputeActions aggregates ident.Actions from all stored inline policies
// for a user. Returns (nil, nil) when the credential manager is unavailable
// (caller should keep existing actions). Returns a non-nil error on store
// failures so callers can abort the mutation.
func (e *EmbeddedIamApi) recomputeActions(ctx context.Context, userName string) ([]string, error) {
	if e.credentialManager == nil {
		return nil, nil
	}
	policyNames, err := e.credentialManager.ListUserInlinePolicies(ctx, userName)
	if err != nil {
		return nil, fmt.Errorf("list inline policies for user %s: %w", userName, err)
	}
	if len(policyNames) == 0 {
		return nil, nil
	}
	actionSet := make(map[string]bool)
	var aggregated []string
	for _, name := range policyNames {
		doc, err := e.credentialManager.GetUserInlinePolicy(ctx, userName, name)
		if err != nil {
			return nil, fmt.Errorf("read inline policy %q for user %s: %w", name, userName, err)
		}
		if doc == nil {
			continue
		}
		actions, err := e.getActions(doc)
		if err != nil {
			glog.Warningf("recomputeActions: failed to parse inline policy %q for user %s: %v", name, userName, err)
			continue
		}
		for _, a := range actions {
			if !actionSet[a] {
				actionSet[a] = true
				aggregated = append(aggregated, a)
			}
		}
	}
	return aggregated, nil
}

// PutUserPolicy attaches a policy to a user.
func (e *EmbeddedIamApi) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamPutUserPolicyResponse, *iamError) {
	resp := &iamPutUserPolicyResponse{}
	userName := values.Get("UserName")
	policyName := values.Get("PolicyName")
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	policyDocumentString := values.Get("PolicyDocument")
	policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	actions, err := e.getActions(&policyDocument)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	glog.V(3).Infof("PutUserPolicy: actions=%v", actions)
	for _, ident := range s3cfg.Identities {
		if userName != ident.Name {
			continue
		}

		// Persist the original policy document for lossless round-trip via GetUserPolicy.
		// This must succeed before updating ident.Actions to keep both in sync.
		ctx := context.Background()
		if e.credentialManager != nil {
			if storeErr := e.credentialManager.PutUserInlinePolicy(ctx, userName, policyName, policyDocument); storeErr != nil {
				return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: storeErr}
			}
		}

		// Recompute ident.Actions from ALL stored inline policies so that
		// multiple policies are properly aggregated for enforcement.
		aggregated, recomputeErr := e.recomputeActions(ctx, userName)
		if recomputeErr != nil {
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: recomputeErr}
		}
		if aggregated != nil {
			ident.Actions = aggregated
		} else {
			ident.Actions = actions
		}
		return resp, nil
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the user with name %s cannot be found", userName)}
}

// GetUserPolicy gets the policy attached to a user.
func (e *EmbeddedIamApi) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetUserPolicyResponse, *iamError) {
	resp := &iamGetUserPolicyResponse{}
	userName := values.Get("UserName")
	policyName := values.Get("PolicyName")
	for _, ident := range s3cfg.Identities {
		if userName != ident.Name {
			continue
		}

		resp.GetUserPolicyResult.UserName = userName
		resp.GetUserPolicyResult.PolicyName = policyName

		// Try to retrieve the stored inline policy document for a lossless round-trip
		if e.credentialManager != nil {
			ctx := context.Background()
			storedDoc, storeErr := e.credentialManager.GetUserInlinePolicy(ctx, userName, policyName)
			if storeErr != nil {
				glog.Warningf("GetUserPolicy: failed to read stored inline policy %q for user %s: %v; falling back to reconstruction", policyName, userName, storeErr)
			}
			if storeErr == nil && storedDoc != nil {
				policyDocumentJSON, err := json.Marshal(storedDoc)
				if err != nil {
					return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
				}
				resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON)
				return resp, nil
			}
		}

		// Fallback: reconstruct from ident.Actions (lossy - fine-grained actions
		// collapse to wildcards like s3:Get*, s3:Put*, s3:List*)
		if len(ident.Actions) == 0 {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("no actions found")}
		}

		policyDocument := policy_engine.PolicyDocument{Version: iamPolicyDocumentVersion}
		statements := make(map[string][]string)
		seenAction := make(map[string]map[string]bool)
		for _, action := range ident.Actions {
			// Action format: "ActionType" (global) or "ActionType:bucket" or "ActionType:bucket/path"
			// Use SplitN so the path component (which may contain ':') is preserved intact.
			act := strings.SplitN(action, ":", 2)

			resource := "*"
			if len(act) == 2 {
				// Preserve the stored path verbatim so bucket-level and
				// object-level resources remain distinguishable.
				resource = fmt.Sprintf("arn:aws:s3:::%s", act[1])
			}
			s3Action := fmt.Sprintf("s3:%s", iamMapToIdentitiesAction(act[0]))
			// Dedupe actions per resource
			if seenAction[resource] == nil {
				seenAction[resource] = make(map[string]bool)
			}
			if seenAction[resource][s3Action] {
				continue
			}
			seenAction[resource][s3Action] = true
			statements[resource] = append(statements[resource], s3Action)
		}
		for resource, actions := range statements {
			isEqAction := false
			for i, statement := range policyDocument.Statement {
				if iamStringSlicesEqual(statement.Action.Strings(), actions) {
					policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlicePtr(append(
						policyDocument.Statement[i].Resource.Strings(), resource)...)
					isEqAction = true
					break
				}
			}
			if isEqAction {
				continue
			}
			policyDocumentStatement := policy_engine.PolicyStatement{
				Effect:   policy_engine.PolicyEffectAllow,
				Action:   policy_engine.NewStringOrStringSlice(actions...),
				Resource: policy_engine.NewStringOrStringSlicePtr(resource),
			}
			policyDocument.Statement = append(policyDocument.Statement, policyDocumentStatement)
		}
		policyDocumentJSON, err := json.Marshal(policyDocument)
		if err != nil {
			return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
		}
		resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON)
		return resp, nil
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// DeleteUserPolicy removes the inline policy from a user.
func (e *EmbeddedIamApi) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteUserPolicyResponse, *iamError) {
	resp := &iamDeleteUserPolicyResponse{}
	userName := values.Get("UserName")
	policyName := values.Get("PolicyName")
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			ctx := context.Background()

			// Remove the stored inline policy document.
			// Must succeed before updating ident.Actions to keep both in sync.
			if e.credentialManager != nil {
				if err := e.credentialManager.DeleteUserInlinePolicy(ctx, userName, policyName); err != nil {
					return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
				}
			}

			// Recompute ident.Actions from remaining inline policies
			aggregated, recomputeErr := e.recomputeActions(ctx, userName)
			if recomputeErr != nil {
				return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: recomputeErr}
			}
			if aggregated != nil {
				ident.Actions = aggregated
			} else {
				ident.Actions = nil
			}
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// ListUserPolicies lists the names of inline policies attached to a user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserPolicies.html
func (e *EmbeddedIamApi) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListUserPoliciesResponse, *iamError) {
	resp := &iamListUserPoliciesResponse{}
	userName := values.Get("UserName")
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			// Try to list stored inline policy names
			if e.credentialManager != nil {
				ctx := context.Background()
				if names, err := e.credentialManager.ListUserInlinePolicies(ctx, userName); err == nil && len(names) > 0 {
					resp.ListUserPoliciesResult.PolicyNames = names
					resp.ListUserPoliciesResult.IsTruncated = false
					return resp, nil
				}
			}
			// Fallback: infer a single policy name from actions
			if len(ident.Actions) > 0 {
				resp.ListUserPoliciesResult.PolicyNames = []string{userName + "_policy"}
			}
			resp.ListUserPoliciesResult.IsTruncated = false
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// parseTagListParams reads AWS query-encoded "Tags.member.N.Key" / "Tags.member.N.Value"
// pairs from form values, returning them in numeric order.
func parseTagListParams(values url.Values) ([]*iam_pb.UserTag, *iamError) {
	type indexed struct {
		idx   int
		key   string
		value string
	}
	indexes := make(map[int]*indexed)
	for name, vs := range values {
		if len(vs) == 0 || !strings.HasPrefix(name, "Tags.member.") {
			continue
		}
		rest := strings.TrimPrefix(name, "Tags.member.")
		dot := strings.Index(rest, ".")
		if dot < 0 {
			continue
		}
		n, err := strconv.Atoi(rest[:dot])
		if err != nil || n < 1 {
			continue
		}
		entry, ok := indexes[n]
		if !ok {
			entry = &indexed{idx: n}
			indexes[n] = entry
		}
		switch rest[dot+1:] {
		case "Key":
			entry.key = vs[0]
		case "Value":
			entry.value = vs[0]
		}
	}
	keys := make([]int, 0, len(indexes))
	for k := range indexes {
		keys = append(keys, k)
	}
	sort.Ints(keys)
	tags := make([]*iam_pb.UserTag, 0, len(keys))
	for _, k := range keys {
		t := indexes[k]
		if t.key == "" {
			return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("tag key cannot be empty")}
		}
		if len(t.key) > MaxUserTagKeyLength {
			return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag key %q exceeds %d characters", t.key, MaxUserTagKeyLength)}
		}
		if len(t.value) > MaxUserTagValueLength {
			return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag value for key %q exceeds %d characters", t.key, MaxUserTagValueLength)}
		}
		tags = append(tags, &iam_pb.UserTag{Key: t.key, Value: t.value})
	}
	return tags, nil
}

// parseTagKeysParams reads AWS query-encoded "TagKeys.member.N" entries and
// validates each entry is a non-empty key within MaxUserTagKeyLength.
func parseTagKeysParams(values url.Values) ([]string, *iamError) {
	entries := make(map[int]string)
	for name, vs := range values {
		if len(vs) == 0 || !strings.HasPrefix(name, "TagKeys.member.") {
			continue
		}
		rest := strings.TrimPrefix(name, "TagKeys.member.")
		n, err := strconv.Atoi(rest)
		if err != nil || n < 1 {
			continue
		}
		entries[n] = vs[0]
	}
	keys := make([]int, 0, len(entries))
	for k := range entries {
		keys = append(keys, k)
	}
	sort.Ints(keys)
	result := make([]string, 0, len(keys))
	for _, k := range keys {
		key := entries[k]
		if key == "" {
			return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("tag key cannot be empty")}
		}
		if len(key) > MaxUserTagKeyLength {
			return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag key %q exceeds %d characters", key, MaxUserTagKeyLength)}
		}
		result = append(result, key)
	}
	return result, nil
}

// mergeUserTags overwrites existing tags with matching keys and appends new ones,
// preserving original order for stable iteration.
func mergeUserTags(existing []*iam_pb.UserTag, incoming []*iam_pb.UserTag) []*iam_pb.UserTag {
	index := make(map[string]int, len(existing))
	merged := make([]*iam_pb.UserTag, 0, len(existing)+len(incoming))
	for _, t := range existing {
		merged = append(merged, &iam_pb.UserTag{Key: t.Key, Value: t.Value})
		index[t.Key] = len(merged) - 1
	}
	for _, t := range incoming {
		if i, ok := index[t.Key]; ok {
			merged[i].Value = t.Value
			continue
		}
		index[t.Key] = len(merged)
		merged = append(merged, &iam_pb.UserTag{Key: t.Key, Value: t.Value})
	}
	return merged
}

// TagUser adds or updates tags on an IAM user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_TagUser.html
func (e *EmbeddedIamApi) TagUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamTagUserResponse, *iamError) {
	resp := &iamTagUserResponse{}
	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	incoming, iamErr := parseTagListParams(values)
	if iamErr != nil {
		return resp, iamErr
	}
	if len(incoming) == 0 {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("at least one tag is required")}
	}
	// Reject duplicate keys within the same request to match AWS semantics.
	seen := make(map[string]bool, len(incoming))
	for _, t := range incoming {
		if seen[t.Key] {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("duplicate tag key %q", t.Key)}
		}
		seen[t.Key] = true
	}
	for _, ident := range s3cfg.Identities {
		if ident.Name != userName {
			continue
		}
		merged := mergeUserTags(ident.Tags, incoming)
		if len(merged) > MaxUserTags {
			return resp, &iamError{Code: iam.ErrCodeLimitExceededException, Error: fmt.Errorf("cannot exceed %d tags per user", MaxUserTags)}
		}
		ident.Tags = merged
		return resp, nil
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// UntagUser removes the named tags from an IAM user. Unknown keys are silently ignored.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_UntagUser.html
func (e *EmbeddedIamApi) UntagUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUntagUserResponse, *iamError) {
	resp := &iamUntagUserResponse{}
	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	keys, iamErr := parseTagKeysParams(values)
	if iamErr != nil {
		return resp, iamErr
	}
	if len(keys) == 0 {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("at least one TagKeys entry is required")}
	}
	for _, ident := range s3cfg.Identities {
		if ident.Name != userName {
			continue
		}
		if len(ident.Tags) == 0 {
			return resp, nil
		}
		toRemove := make(map[string]bool, len(keys))
		for _, k := range keys {
			toRemove[k] = true
		}
		filtered := make([]*iam_pb.UserTag, 0, len(ident.Tags))
		for _, t := range ident.Tags {
			if toRemove[t.Key] {
				continue
			}
			filtered = append(filtered, t)
		}
		ident.Tags = filtered
		return resp, nil
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// ListUserTags returns the tags attached to an IAM user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserTags.html
// Pagination is not implemented: tag counts are bounded by MaxUserTags so the
// full set always fits in a single response.
func (e *EmbeddedIamApi) ListUserTags(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListUserTagsResponse, *iamError) {
	resp := &iamListUserTagsResponse{}
	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	for _, ident := range s3cfg.Identities {
		if ident.Name != userName {
			continue
		}
		tags := make([]*iamTag, 0, len(ident.Tags))
		for _, t := range ident.Tags {
			tags = append(tags, &iamTag{Key: t.Key, Value: t.Value})
		}
		resp.ListUserTagsResult.Tags = tags
		resp.ListUserTagsResult.IsTruncated = false
		return resp, nil
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// AttachUserPolicy attaches a managed policy to a user.
func (e *EmbeddedIamApi) AttachUserPolicy(ctx context.Context, values url.Values) (*iamAttachUserPolicyResponse, *iamError) {
	resp := &iamAttachUserPolicyResponse{}

	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}

	policyArn := values.Get("PolicyArn")
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}

	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}

	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}

	attachedPolicies, err := e.credentialManager.ListAttachedUserPolicies(ctx, userName)
	if err != nil {
		if errors.Is(err, credential.ErrUserNotFound) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
		}
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	for _, attached := range attachedPolicies {
		if attached == policyName {
			return resp, nil
		}
	}
	if len(attachedPolicies) >= MaxManagedPoliciesPerUser {
		return resp, &iamError{
			Code:  iam.ErrCodeLimitExceededException,
			Error: fmt.Errorf("cannot attach more than %d managed policies to user %s", MaxManagedPoliciesPerUser, userName),
		}
	}

	if err := e.credentialManager.AttachUserPolicy(ctx, userName, policyName); err != nil {
		if errors.Is(err, credential.ErrUserNotFound) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
		}
		if errors.Is(err, credential.ErrPolicyNotFound) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
		}
		if errors.Is(err, credential.ErrPolicyAlreadyAttached) {
			// AWS IAM is idempotent for AttachUserPolicy
			return resp, nil
		}
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	// Best-effort refresh: log any failures but don't fail the API call since the mutation succeeded
	if err := e.refreshIAMConfiguration(); err != nil {
		glog.Warningf("Failed to refresh IAM configuration after attaching policy %s to user %s: %v", policyName, userName, err)
	}

	return resp, nil
}

// DetachUserPolicy detaches a managed policy from a user.
func (e *EmbeddedIamApi) DetachUserPolicy(ctx context.Context, values url.Values) (*iamDetachUserPolicyResponse, *iamError) {
	resp := &iamDetachUserPolicyResponse{}

	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}

	policyArn := values.Get("PolicyArn")
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}

	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}

	policy, err := e.credentialManager.GetPolicy(ctx, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}

	if err := e.credentialManager.DetachUserPolicy(ctx, userName, policyName); err != nil {
		if errors.Is(err, credential.ErrUserNotFound) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
		}
		if errors.Is(err, credential.ErrPolicyNotAttached) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not attached to user %s", policyName, userName)}
		}
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	// Best-effort refresh: log any failures but don't fail the API call since the mutation succeeded
	if err := e.refreshIAMConfiguration(); err != nil {
		glog.Warningf("Failed to refresh IAM configuration after detaching policy %s from user %s: %v", policyName, userName, err)
	}

	return resp, nil
}

// ListAttachedUserPolicies lists managed policies attached to a user.
func (e *EmbeddedIamApi) ListAttachedUserPolicies(ctx context.Context, values url.Values) (*iamListAttachedUserPoliciesResponse, *iamError) {
	resp := &iamListAttachedUserPoliciesResponse{}

	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}

	pathPrefix := values.Get("PathPrefix")
	if pathPrefix == "" {
		pathPrefix = "/"
	}

	maxItems := 0
	if maxItemsStr := values.Get("MaxItems"); maxItemsStr != "" {
		parsedMaxItems, err := strconv.Atoi(maxItemsStr)
		if err != nil || parsedMaxItems <= 0 {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("MaxItems must be a positive integer")}
		}
		maxItems = parsedMaxItems
	}
	marker := values.Get("Marker")

	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}

	policyNames, err := e.credentialManager.ListAttachedUserPolicies(ctx, userName)
	if err != nil {
		if errors.Is(err, credential.ErrUserNotFound) {
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
		}
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}

	var attachedPolicies []*iam.AttachedPolicy
	for _, attachedPolicyName := range policyNames {
		// Policy paths are not tracked in the current configuration, so PathPrefix
		// filtering is not supported yet. Always return the policy for now.
		policyNameCopy := attachedPolicyName
		policyArn := iamPolicyArn(attachedPolicyName)
		policyArnCopy := policyArn
		attachedPolicies = append(attachedPolicies, &iam.AttachedPolicy{
			PolicyName: &policyNameCopy,
			PolicyArn:  &policyArnCopy,
		})
	}

	start := 0
	markerFound := false
	if marker != "" {
		for i, p := range attachedPolicies {
			if p.PolicyName != nil && *p.PolicyName == marker {
				start = i + 1
				markerFound = true
				break
			}
		}
		if !markerFound && len(attachedPolicies) > 0 {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("marker %s not found", marker)}
		}
	}
	if start > 0 && start < len(attachedPolicies) {
		attachedPolicies = attachedPolicies[start:]
	} else if start >= len(attachedPolicies) {
		attachedPolicies = nil
	}

	if maxItems > 0 && len(attachedPolicies) > maxItems {
		resp.ListAttachedUserPoliciesResult.AttachedPolicies = attachedPolicies[:maxItems]
		resp.ListAttachedUserPoliciesResult.IsTruncated = true
		if name := resp.ListAttachedUserPoliciesResult.AttachedPolicies[maxItems-1].PolicyName; name != nil {
			resp.ListAttachedUserPoliciesResult.Marker = *name
		}
		return resp, nil
	}

	resp.ListAttachedUserPoliciesResult.AttachedPolicies = attachedPolicies
	resp.ListAttachedUserPoliciesResult.IsTruncated = false
	return resp, nil
}

// SetUserStatus enables or disables a user without deleting them.
// This is a SeaweedFS extension for temporary user suspension, offboarding, etc.
// When a user is disabled, all API requests using their credentials will return AccessDenied.
func (e *EmbeddedIamApi) SetUserStatus(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamSetUserStatusResponse, *iamError) {
	resp := &iamSetUserStatusResponse{}
	userName := values.Get("UserName")
	status := values.Get("Status")

	// Validate UserName
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}

	// Validate Status - must be "Active" or "Inactive"
	if err := iamValidateStatus(status); err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}

	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			// Set disabled based on status: Active = not disabled, Inactive = disabled
			ident.Disabled = (status == iamAccessKeyStatusInactive)
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// UpdateAccessKey updates the status of an access key (Active or Inactive).
// This allows key rotation workflows where old keys are deactivated before deletion.
func (e *EmbeddedIamApi) UpdateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUpdateAccessKeyResponse, *iamError) {
	resp := &iamUpdateAccessKeyResponse{}
	userName := values.Get("UserName")
	accessKeyId := values.Get("AccessKeyId")
	status := values.Get("Status")

	// Validate required parameters
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	if accessKeyId == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId is required")}
	}
	if err := iamValidateStatus(status); err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}

	for _, ident := range s3cfg.Identities {
		if ident.Name != userName {
			continue
		}
		for _, cred := range ident.Credentials {
			if cred.AccessKey == accessKeyId {
				cred.Status = status
				return resp, nil
			}
		}
		// User found but access key not found
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the access key with id %s for user %s cannot be found", accessKeyId, userName)}
	}

	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}

// findIdentityByName is a helper function to find an identity by name.
// Returns the identity or nil if not found.
func findIdentityByName(s3cfg *iam_pb.S3ApiConfiguration, name string) *iam_pb.Identity {
	for _, ident := range s3cfg.Identities {
		if ident.Name == name {
			return ident
		}
	}
	return nil
}

// CreateServiceAccount creates a new service account for a user.
func (e *EmbeddedIamApi) CreateServiceAccount(s3cfg *iam_pb.S3ApiConfiguration, values url.Values, createdBy string) (*iamCreateServiceAccountResponse, *iamError) {
	resp := &iamCreateServiceAccountResponse{}
	parentUser := values.Get("ParentUser")
	description := values.Get("Description")
	expirationStr := values.Get("Expiration") // Unix timestamp as string

	if parentUser == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("ParentUser is required")}
	}

	// Validate description length
	if len(description) > MaxDescriptionLength {
		return resp, &iamError{
			Code:  iam.ErrCodeInvalidInputException,
			Error: fmt.Errorf("description exceeds maximum length of %d characters", MaxDescriptionLength),
		}
	}

	// Verify parent user exists
	parentIdent := findIdentityByName(s3cfg, parentUser)
	if parentIdent == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, parentUser)}
	}

	// Check service account limit per user
	if len(parentIdent.ServiceAccountIds) >= MaxServiceAccountsPerUser {
		return resp, &iamError{
			Code: iam.ErrCodeLimitExceededException,
			Error: fmt.Errorf("user %s has reached the maximum limit of %d service accounts",
				parentUser, MaxServiceAccountsPerUser),
		}
	}

	// Generate a unique service account ID in the format required by
	// credential.ValidateServiceAccountId: sa:<parent>:<uuid>. 16 bytes of
	// randomness (hex-encoded) matches the shell command's generator.
	var idBytes [16]byte
	if _, err := rand.Read(idBytes[:]); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate ID: %w", err)}
	}
	saId := fmt.Sprintf("%s:%s:%s", ServiceAccountIDPrefix, parentUser, hex.EncodeToString(idBytes[:]))

	// Fail closed if the generated ID wouldn't pass the persistence-layer
	// validator — better a 400 here than an opaque 500 at save time. This
	// guards against parent-user values that slipped past earlier
	// validation (e.g., containing `:` or whitespace).
	if err := credential.ValidateServiceAccountId(saId); err != nil {
		return resp, &iamError{
			Code:  iam.ErrCodeInvalidInputException,
			Error: fmt.Errorf("generated invalid service account ID %q: %w", saId, err),
		}
	}

	// Generate access key ID with correct length (20 chars total including prefix)
	// AWS access keys are always 20 characters: 4-char prefix (ABIA) + 16 random chars
	accessKeyId, err := iamStringWithCharset(AccessKeyLength-len(ServiceAccountKeyPrefix), iamCharsetUpper)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)}
	}
	accessKeyId = ServiceAccountKeyPrefix + accessKeyId

	secretAccessKey, err := iamStringWithCharset(SecretKeyLength, iamCharset)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate secret key: %w", err)}
	}

	// Parse expiration if provided
	var expiration int64
	if expirationStr != "" {
		var err error
		expiration, err = strconv.ParseInt(expirationStr, 10, 64)
		if err != nil {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid expiration format: %w", err)}
		}
		if expiration > 0 && expiration < time.Now().Unix() {
			return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("expiration must be in the future")}
		}
	}

	now := time.Now()

	// Copy parent's actions to avoid shared slice reference
	actions := make([]string, len(parentIdent.Actions))
	copy(actions, parentIdent.Actions)

	sa := &iam_pb.ServiceAccount{
		Id:          saId,
		ParentUser:  parentUser,
		Description: description,
		Credential: &iam_pb.Credential{
			AccessKey: accessKeyId,
			SecretKey: secretAccessKey,
			Status:    iamAccessKeyStatusActive,
		},
		Actions:    actions, // Independent copy of parent's actions
		Expiration: expiration,
		Disabled:   false,
		CreatedAt:  now.Unix(),
		CreatedBy:  createdBy,
	}

	s3cfg.ServiceAccounts = append(s3cfg.ServiceAccounts, sa)
	parentIdent.ServiceAccountIds = append(parentIdent.ServiceAccountIds, saId)

	// Build response
	resp.CreateServiceAccountResult.ServiceAccount = iamServiceAccountInfo{
		ServiceAccountId: saId,
		ParentUser:       parentUser,
		Description:      description,
		AccessKeyId:      accessKeyId,
		SecretAccessKey:  &secretAccessKey,
		Status:           iamAccessKeyStatusActive,
		CreateDate:       now.Format(time.RFC3339),
	}
	if expiration > 0 {
		expStr := time.Unix(expiration, 0).Format(time.RFC3339)
		resp.CreateServiceAccountResult.ServiceAccount.Expiration = &expStr
	}

	return resp, nil
}

// DeleteServiceAccount deletes a service account.
func (e *EmbeddedIamApi) DeleteServiceAccount(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteServiceAccountResponse, *iamError) {
	resp := &iamDeleteServiceAccountResponse{}
	saId := values.Get("ServiceAccountId")

	if saId == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("ServiceAccountId is required")}
	}

	// Find and remove the service account
	for i, sa := range s3cfg.ServiceAccounts {
		if sa.Id == saId {
			// Remove from parent's list
			if parentIdent := findIdentityByName(s3cfg, sa.ParentUser); parentIdent != nil {
				// Remove service account ID from parent's list using filter pattern
				// This avoids mutating the slice during iteration
				filtered := parentIdent.ServiceAccountIds[:0]
				for _, id := range parentIdent.ServiceAccountIds {
					if id != saId {
						filtered = append(filtered, id)
					}
				}
				parentIdent.ServiceAccountIds = filtered
			}
			// Remove service account
			s3cfg.ServiceAccounts = append(s3cfg.ServiceAccounts[:i], s3cfg.ServiceAccounts[i+1:]...)
			return resp, nil
		}
	}

	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("service account %s not found", saId)}
}

// ListServiceAccounts lists service accounts, optionally filtered by parent user.
func (e *EmbeddedIamApi) ListServiceAccounts(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamListServiceAccountsResponse {
	resp := &iamListServiceAccountsResponse{}
	parentUser := values.Get("ParentUser") // Optional filter

	for _, sa := range s3cfg.ServiceAccounts {
		if parentUser != "" && sa.ParentUser != parentUser {
			continue
		}
		if sa.Credential == nil {
			glog.Warningf("Service account %s has nil credential, skipping", sa.Id)
			continue
		}
		status := iamAccessKeyStatusActive
		if sa.Disabled {
			status = iamAccessKeyStatusInactive
		}
		info := &iamServiceAccountInfo{
			ServiceAccountId: sa.Id,
			ParentUser:       sa.ParentUser,
			Description:      sa.Description,
			AccessKeyId:      sa.Credential.AccessKey,
			Status:           status,
			CreateDate:       time.Unix(sa.CreatedAt, 0).Format(time.RFC3339),
		}
		if sa.Expiration > 0 {
			expStr := time.Unix(sa.Expiration, 0).Format(time.RFC3339)
			info.Expiration = &expStr
		}
		resp.ListServiceAccountsResult.ServiceAccounts = append(resp.ListServiceAccountsResult.ServiceAccounts, info)
	}

	return resp
}

// GetServiceAccount retrieves a service account by ID.
func (e *EmbeddedIamApi) GetServiceAccount(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetServiceAccountResponse, *iamError) {
	resp := &iamGetServiceAccountResponse{}
	saId := values.Get("ServiceAccountId")

	if saId == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("ServiceAccountId is required")}
	}

	for _, sa := range s3cfg.ServiceAccounts {
		if sa.Id == saId {
			if sa.Credential == nil {
				return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("service account %s has no credentials", saId)}
			}
			status := iamAccessKeyStatusActive
			if sa.Disabled {
				status = iamAccessKeyStatusInactive
			}
			resp.GetServiceAccountResult.ServiceAccount = iamServiceAccountInfo{
				ServiceAccountId: sa.Id,
				ParentUser:       sa.ParentUser,
				Description:      sa.Description,
				AccessKeyId:      sa.Credential.AccessKey,
				Status:           status,
				CreateDate:       time.Unix(sa.CreatedAt, 0).Format(time.RFC3339),
			}
			if sa.Expiration > 0 {
				expStr := time.Unix(sa.Expiration, 0).Format(time.RFC3339)
				resp.GetServiceAccountResult.ServiceAccount.Expiration = &expStr
			}
			return resp, nil
		}
	}

	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("service account %s not found", saId)}
}

// UpdateServiceAccount updates a service account's status, description, or expiration.
func (e *EmbeddedIamApi) UpdateServiceAccount(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUpdateServiceAccountResponse, *iamError) {
	resp := &iamUpdateServiceAccountResponse{}
	saId := values.Get("ServiceAccountId")
	newStatus := values.Get("Status")
	newDescription := values.Get("Description")
	newExpirationStr := values.Get("Expiration")

	if saId == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("ServiceAccountId is required")}
	}

	for _, sa := range s3cfg.ServiceAccounts {
		if sa.Id == saId {
			// Update status if provided
			if newStatus != "" {
				if err := iamValidateStatus(newStatus); err != nil {
					return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
				}
				sa.Disabled = (newStatus == iamAccessKeyStatusInactive)
			}
			// Update description if provided (check for key existence to allow clearing)
			if _, hasDescription := values["Description"]; hasDescription {
				if len(newDescription) > MaxDescriptionLength {
					return resp, &iamError{
						Code:  iam.ErrCodeInvalidInputException,
						Error: fmt.Errorf("description exceeds maximum length of %d characters", MaxDescriptionLength),
					}
				}
				sa.Description = newDescription
			}
			// Update expiration if provided (check for key existence to allow clearing to 0)
			if _, hasExpiration := values["Expiration"]; hasExpiration {
				if newExpirationStr != "" {
					newExpiration, err := strconv.ParseInt(newExpirationStr, 10, 64)
					if err != nil {
						return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid expiration format: %w", err)}
					}
					// Validate expiration value
					if newExpiration < 0 {
						return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("expiration must not be negative")}
					}
					if newExpiration > 0 && newExpiration < time.Now().Unix() {
						return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("expiration must be in the future")}
					}
					// 0 is explicitly allowed to clear expiration
					sa.Expiration = newExpiration
				} else {
					// Empty string means clear expiration (set to 0 = no expiration)
					sa.Expiration = 0
				}
			}
			return resp, nil
		}
	}

	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("service account %s not found", saId)}
}

// Group Management Handlers

func (e *EmbeddedIamApi) CreateGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamCreateGroupResponse, *iamError) {
	resp := &iamCreateGroupResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("group %s already exists", groupName)}
		}
	}
	s3cfg.Groups = append(s3cfg.Groups, &iam_pb.Group{Name: groupName})
	resp.CreateGroupResult.Group.GroupName = &groupName
	return resp, nil
}

func (e *EmbeddedIamApi) DeleteGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteGroupResponse, *iamError) {
	resp := &iamDeleteGroupResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	for i, g := range s3cfg.Groups {
		if g.Name == groupName {
			if len(g.Members) > 0 {
				return resp, &iamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete group %s: group has %d member(s). Remove all members first", groupName, len(g.Members))}
			}
			if len(g.PolicyNames) > 0 {
				return resp, &iamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete group %s: group has %d attached policy(ies). Detach all policies first", groupName, len(g.PolicyNames))}
			}
			s3cfg.Groups = append(s3cfg.Groups[:i], s3cfg.Groups[i+1:]...)
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) UpdateGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUpdateGroupResponse, *iamError) {
	resp := &iamUpdateGroupResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			if disabled := values.Get("Disabled"); disabled != "" {
				if disabled != "true" && disabled != "false" {
					return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("Disabled must be 'true' or 'false'")}
				}
				g.Disabled = disabled == "true"
			}
			if newName := values.Get("NewGroupName"); newName != "" && newName != g.Name {
				for _, other := range s3cfg.Groups {
					if other.Name == newName {
						return resp, &iamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("group %s already exists", newName)}
					}
				}
				g.Name = newName
			}
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) GetGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetGroupResponse, *iamError) {
	resp := &iamGetGroupResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			resp.GetGroupResult.Group.GroupName = &g.Name
			for _, member := range g.Members {
				user := iamlib.NewUser(member)
				resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &user)
			}
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) ListGroups(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamListGroupsResponse {
	resp := &iamListGroupsResponse{}
	for _, g := range s3cfg.Groups {
		name := g.Name
		resp.ListGroupsResult.Groups = append(resp.ListGroupsResult.Groups, &iam.Group{GroupName: &name})
	}
	return resp
}

func (e *EmbeddedIamApi) AddUserToGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamAddUserToGroupResponse, *iamError) {
	resp := &iamAddUserToGroupResponse{}
	groupName := values.Get("GroupName")
	userName := values.Get("UserName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	// Verify user exists
	userFound := false
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			userFound = true
			break
		}
	}
	if !userFound {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s does not exist", userName)}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			// Check if already a member (idempotent)
			for _, m := range g.Members {
				if m == userName {
					return resp, nil
				}
			}
			g.Members = append(g.Members, userName)
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) RemoveUserFromGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamRemoveUserFromGroupResponse, *iamError) {
	resp := &iamRemoveUserFromGroupResponse{}
	groupName := values.Get("GroupName")
	userName := values.Get("UserName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			for i, m := range g.Members {
				if m == userName {
					g.Members = append(g.Members[:i], g.Members[i+1:]...)
					return resp, nil
				}
			}
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s is not a member of group %s", userName, groupName)}
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) AttachGroupPolicy(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamAttachGroupPolicyResponse, *iamError) {
	resp := &iamAttachGroupPolicyResponse{}
	groupName := values.Get("GroupName")
	policyArn := values.Get("PolicyArn")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	// Verify policy exists via credential manager
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not available to validate policy %s", policyName)}
	}
	policy, pErr := e.credentialManager.GetPolicy(ctx, policyName)
	if pErr != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to look up policy %s: %w", policyName, pErr)}
	}
	if policy == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			// Check if already attached (idempotent)
			for _, p := range g.PolicyNames {
				if p == policyName {
					return resp, nil
				}
			}
			g.PolicyNames = append(g.PolicyNames, policyName)
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) DetachGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDetachGroupPolicyResponse, *iamError) {
	resp := &iamDetachGroupPolicyResponse{}
	groupName := values.Get("GroupName")
	policyArn := values.Get("PolicyArn")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	policyName, err := iamPolicyNameFromArn(policyArn)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			for i, p := range g.PolicyNames {
				if p == policyName {
					g.PolicyNames = append(g.PolicyNames[:i], g.PolicyNames[i+1:]...)
					return resp, nil
				}
			}
			return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s is not attached to group %s", policyName, groupName)}
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) ListAttachedGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListAttachedGroupPoliciesResponse, *iamError) {
	resp := &iamListAttachedGroupPoliciesResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			for _, policyName := range g.PolicyNames {
				pn := policyName
				policyArn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
				resp.ListAttachedGroupPoliciesResult.AttachedPolicies = append(resp.ListAttachedGroupPoliciesResult.AttachedPolicies, &iam.AttachedPolicy{
					PolicyName: &pn,
					PolicyArn:  &policyArn,
				})
			}
			return resp, nil
		}
	}
	return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
}

func (e *EmbeddedIamApi) ListGroupsForUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListGroupsForUserResponse, *iamError) {
	resp := &iamListGroupsForUserResponse{}
	userName := values.Get("UserName")
	if userName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
	}
	// Verify user exists
	userFound := false
	for _, ident := range s3cfg.Identities {
		if ident.Name == userName {
			userFound = true
			break
		}
	}
	if !userFound {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s does not exist", userName)}
	}
	// Build from s3cfg.Groups for consistency with freshly loaded config
	for _, g := range s3cfg.Groups {
		for _, m := range g.Members {
			if m == userName {
				name := g.Name
				resp.ListGroupsForUserResult.Groups = append(resp.ListGroupsForUserResult.Groups, &iam.Group{GroupName: &name})
				break
			}
		}
	}
	return resp, nil
}

// groupExists returns true if a group of the given name is present in the
// supplied S3 IAM configuration.
func groupExists(s3cfg *iam_pb.S3ApiConfiguration, groupName string) bool {
	for _, g := range s3cfg.Groups {
		if g.Name == groupName {
			return true
		}
	}
	return false
}

// PutGroupPolicy attaches an inline policy to a group. The document (including
// any Condition block) is persisted via the credential store and registered
// in the IAM policy engine via hydrateRuntimePolicies on the next reload.
func (e *EmbeddedIamApi) PutGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamPutGroupPolicyResponse, *iamError) {
	resp := &iamPutGroupPolicyResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	policyName := values.Get("PolicyName")
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	policyDocumentString := values.Get("PolicyDocument")
	policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
	}
	if !groupExists(s3cfg, groupName) {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
	}
	if err := e.credentialManager.PutGroupInlinePolicy(context.Background(), groupName, policyName, policyDocument); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	// DoActions reloads the IAM configuration for PutGroupPolicy, so the
	// engine registration via hydrateRuntimePolicies happens there. No
	// internal refresh is needed.
	return resp, nil
}

// GetGroupPolicy returns the JSON document for an inline policy on a group.
func (e *EmbeddedIamApi) GetGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetGroupPolicyResponse, *iamError) {
	resp := &iamGetGroupPolicyResponse{}
	groupName := values.Get("GroupName")
	policyName := values.Get("PolicyName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	if !groupExists(s3cfg, groupName) {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	doc, err := e.credentialManager.GetGroupInlinePolicy(context.Background(), groupName, policyName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	if doc == nil {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found on group %s", policyName, groupName)}
	}
	docJSON, err := json.Marshal(doc)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	resp.GetGroupPolicyResult.GroupName = groupName
	resp.GetGroupPolicyResult.PolicyName = policyName
	resp.GetGroupPolicyResult.PolicyDocument = string(docJSON)
	return resp, nil
}

// DeleteGroupPolicy removes an inline policy from a group.
func (e *EmbeddedIamApi) DeleteGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteGroupPolicyResponse, *iamError) {
	resp := &iamDeleteGroupPolicyResponse{}
	groupName := values.Get("GroupName")
	policyName := values.Get("PolicyName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	if policyName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
	}
	if !groupExists(s3cfg, groupName) {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	if err := e.credentialManager.DeleteGroupInlinePolicy(context.Background(), groupName, policyName); err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	// DoActions reloads the IAM configuration for DeleteGroupPolicy, so the
	// engine deregistration via hydrateRuntimePolicies happens there. No
	// internal refresh is needed.
	return resp, nil
}

// ListGroupPolicies returns the names of inline policies attached to a group.
func (e *EmbeddedIamApi) ListGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListGroupPoliciesResponse, *iamError) {
	resp := &iamListGroupPoliciesResponse{}
	groupName := values.Get("GroupName")
	if groupName == "" {
		return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
	}
	if !groupExists(s3cfg, groupName) {
		return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
	}
	if e.credentialManager == nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
	}
	names, err := e.credentialManager.ListGroupInlinePolicies(context.Background(), groupName)
	if err != nil {
		return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
	}
	resp.ListGroupPoliciesResult.PolicyNames = names
	resp.ListGroupPoliciesResult.IsTruncated = false
	return resp, nil
}

// handleImplicitUsername adds username who signs the request to values if 'username' is not specified.
// According to AWS documentation: "If you do not specify a user name, IAM determines the user name
// implicitly based on the Amazon Web Services access key ID signing the request."
// This function extracts the AccessKeyId from the SigV4 credential and looks up the corresponding
// identity name in the credential store.
func (e *EmbeddedIamApi) handleImplicitUsername(r *http.Request, values url.Values) {
	if len(r.Header["Authorization"]) == 0 || values.Get("UserName") != "" {
		return
	}
	// Log presence of auth header without exposing sensitive signature material
	glog.V(4).Infof("Authorization header present, extracting access key")
	// Parse AWS SigV4 Authorization header format:
	// "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/iam/aws4_request, ..."
	s := strings.Split(r.Header["Authorization"][0], "Credential=")
	if len(s) < 2 {
		return
	}
	s = strings.Split(s[1], ",")
	if len(s) < 1 {
		return
	}
	s = strings.Split(s[0], "/")
	if len(s) < 1 {
		return
	}
	// s[0] is the AccessKeyId
	accessKeyId := s[0]
	if accessKeyId == "" {
		return
	}
	// Nil-guard: ensure iam is initialized before lookup
	if e.iam == nil {
		glog.V(4).Infof("IAM not initialized, cannot look up access key")
		return
	}
	// Look up the identity by access key to get the username
	identity, _, found := e.iam.LookupByAccessKey(accessKeyId)
	if !found {
		// Mask access key in logs - show only first 4 chars
		maskedKey := accessKeyId
		if len(accessKeyId) > 4 {
			maskedKey = accessKeyId[:4] + "***"
		}
		glog.V(4).Infof("Access key %s not found in credential store", maskedKey)
		return
	}
	values.Set("UserName", identity.Name)
}

// iamSelfServiceActions are actions that users can perform on their own resources without admin rights.
// According to AWS IAM, users can manage their own access keys without requiring full admin permissions.
var iamSelfServiceActions = map[string]bool{
	"CreateAccessKey": true,
	"DeleteAccessKey": true,
	"ListAccessKeys":  true,
	"GetUser":         true,
	"UpdateAccessKey": true,
}

// iamRequiresAdminForOthers returns true if the action requires admin rights when operating on other users.
func iamRequiresAdminForOthers(action string) bool {
	return iamSelfServiceActions[action]
}

// AuthorizeIamAction authorizes an IAM management action for identity, with
// targetUserName taken from the request's UserName parameter.
//
// IAM management is not part of the S3 data plane, so the grant is checked as
// iam:<Action>. A coarse S3 action would instead be matched by an ordinary
// data-plane policy, which says nothing about administering the credential
// store.
//
// Users may run self-service actions against their own identity without any
// IAM grant, matching AWS.
func (iam *IdentityAccessManagement) AuthorizeIamAction(r *http.Request, identity *Identity, action, targetUserName string) s3err.ErrorCode {
	// The anonymous identity has no user of its own, so every self-service
	// action it names would run against someone else's.
	if identity == nil || identity.Name == s3_constants.AccountAnonymousId {
		return s3err.ErrAccessDenied
	}
	if iamRequiresAdminForOthers(action) && (targetUserName == "" || targetUserName == identity.Name) {
		return s3err.ErrNone
	}
	if identity.isAdmin() {
		return s3err.ErrNone
	}
	return iam.VerifyActionPermission(r, identity, Action("iam:"+action), "arn:aws:iam:::*", "")
}

// AuthIamManagement authenticates an IAM management request and authorizes the
// action it carries. It is the entry point for both IAM API surfaces — the
// embedded one on the S3 port and the standalone `weed iam` server — so the two
// cannot drift apart.
func (iam *IdentityAccessManagement) AuthIamManagement(f http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// If auth is not enabled, allow all
		if !iam.isEnabled() {
			f(w, r)
			return
		}

		// Authenticate BEFORE parsing form.
		// ParseForm() reads and consumes the request body, but signature verification
		// needs to hash the body for IAM requests (service != "s3").
		// The streamHashRequestBody function in auth_signature_v4.go preserves the body
		// after reading it, so ParseForm() will work correctly after authentication.
		identity, errCode := iam.AuthSignatureOnly(r)
		if errCode != s3err.ErrNone {
			s3err.WriteErrorResponse(w, r, errCode)
			return
		}

		// Now parse form to get Action and UserName (body was preserved by auth)
		if err := r.ParseForm(); err != nil {
			s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
			return
		}

		// UserName comes from the body only, the same place the handlers read it
		// from, so the authorized target and the acted-on target cannot differ.
		if errCode := iam.AuthorizeIamAction(r, identity, r.Form.Get("Action"), r.PostForm.Get("UserName")); errCode != s3err.ErrNone {
			s3err.WriteErrorResponse(w, r, errCode)
			return
		}

		if identity.Name != "" {
			r = r.WithContext(recordIdentityInContext(r, identity))
		}

		f(w, r)
	}
}

// AuthIam provides IAM-specific authentication that allows self-service operations.
// Users can manage their own access keys without admin rights, but need admin for operations on other users.
// The action parameter is accepted for interface compatibility with cb.Limit but is not used
// since IAM permission checking is done based on the IAM Action parameter in the request.
func (e *EmbeddedIamApi) AuthIam(f http.HandlerFunc, _ Action) http.HandlerFunc {
	return e.iam.AuthIamManagement(f)
}

// ExecuteAction executes an IAM action with the given values.
// If skipPersist is true, the changed configuration is not saved to the persistent store.
// reqID is set on the response; if empty, a new request ID is generated.
func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, skipPersist bool, reqID string) (iamlib.RequestIDSetter, *iamError) {
	if reqID == "" {
		reqID = request_id.New()
	}
	// Lock to prevent concurrent read-modify-write race conditions
	e.policyLock.Lock()
	defer e.policyLock.Unlock()

	action := values.Get("Action")
	if e.readOnly {
		switch action {
		case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListUserPolicies", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "ListServiceAccounts", "GetServiceAccount",
			"GetGroup", "ListGroups", "ListAttachedGroupPolicies", "GetGroupPolicy", "ListGroupPolicies", "ListGroupsForUser",
			"ListUserTags",
			actionListOpenIDConnectProviders, actionGetOpenIDConnectProvider:
			// Allowed read-only actions
		default:
			return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")}
		}
	}

	// OIDC provider actions don't operate on S3ApiConfiguration; dispatch
	// before the unrelated config load + reload churn.
	if response, iamErr, ok := e.dispatchOIDCProviderAction(ctx, values); ok {
		if iamErr != nil {
			return nil, iamErr
		}
		response.SetRequestId(reqID)
		return response, nil
	}

	s3cfg := &iam_pb.S3ApiConfiguration{}
	if err := e.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
		return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrInternalError).Code, Error: fmt.Errorf("failed to get s3 api configuration: %v", err)}
	}

	glog.V(4).Infof("IAM ExecuteAction: %+v", iamlib.RedactSensitiveFormValues(values))
	var response iamlib.RequestIDSetter
	changed := true
	switch values.Get("Action") {
	case "ListUsers":
		response = e.ListUsers(s3cfg, values)
		changed = false
	case "ListAccessKeys":
		// Note: handleImplicitUsername requires request context which we don't have here for gRPC
		// gRPC callers must provide UserName explicitly
		response = e.ListAccessKeys(s3cfg, values)
		changed = false
	case "CreateUser":
		var iamErr *iamError
		response, iamErr = e.CreateUser(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		// Use targeted create to avoid rewriting all existing user files via SaveConfiguration.
		// credentialManager.CreateUser writes only the new user's file.
		// Skip when skipPersist is set: the contract is no persistent write, so leave
		// changed=true and let the tail's `if changed { if !skipPersist { ... } }` block
		// suppress persistence the same way it does for every other action.
		if e.credentialManager != nil && !skipPersist {
			userName := values.Get("UserName")
			if err := e.credentialManager.CreateUser(ctx, &iam_pb.Identity{Name: userName}); err != nil {
				return nil, &iamError{Code: CredentialErrToIamErrCode(err), Error: err}
			}
			changed = false
		}
	case "GetUser":
		userName := values.Get("UserName")
		var iamErr *iamError
		response, iamErr = e.GetUser(s3cfg, userName)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "UpdateUser":
		var iamErr *iamError
		response, iamErr = e.UpdateUser(ctx, s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "DeleteUser":
		userName := values.Get("UserName")
		var iamErr *iamError
		response, iamErr = e.DeleteUser(s3cfg, userName)
		if iamErr != nil {
			return nil, iamErr
		}
	case "CreateAccessKey":
		var iamErr *iamError
		response, iamErr = e.CreateAccessKey(s3cfg, values)
		if iamErr != nil {
			glog.Errorf("CreateAccessKey: %+v", iamErr.Error)
			return nil, iamErr
		}
	case "DeleteAccessKey":
		response = e.DeleteAccessKey(s3cfg, values)
	case "CreatePolicy":
		var iamErr *iamError
		response, iamErr = e.CreatePolicy(ctx, values)
		if iamErr != nil {
			glog.Errorf("CreatePolicy: %+v", iamErr.Error)
			return nil, iamErr
		}
		changed = false
	case "DeletePolicy":
		var iamErr *iamError
		response, iamErr = e.DeletePolicy(ctx, values)
		if iamErr != nil {
			glog.Errorf("DeletePolicy: %+v", iamErr.Error)
			return nil, iamErr
		}
		changed = false
	case "PutUserPolicy":
		var iamErr *iamError
		response, iamErr = e.PutUserPolicy(s3cfg, values)
		if iamErr != nil {
			glog.Errorf("PutUserPolicy: %+v", iamErr.Error)
			return nil, iamErr
		}
	case "GetUserPolicy":
		var iamErr *iamError
		response, iamErr = e.GetUserPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "DeleteUserPolicy":
		var iamErr *iamError
		response, iamErr = e.DeleteUserPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "ListUserPolicies":
		var iamErr *iamError
		response, iamErr = e.ListUserPolicies(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "TagUser":
		var iamErr *iamError
		response, iamErr = e.TagUser(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "UntagUser":
		var iamErr *iamError
		response, iamErr = e.UntagUser(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "ListUserTags":
		var iamErr *iamError
		response, iamErr = e.ListUserTags(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "AttachUserPolicy":
		var iamErr *iamError
		response, iamErr = e.AttachUserPolicy(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "DetachUserPolicy":
		var iamErr *iamError
		response, iamErr = e.DetachUserPolicy(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListAttachedUserPolicies":
		var iamErr *iamError
		response, iamErr = e.ListAttachedUserPolicies(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListPolicies":
		var iamErr *iamError
		response, iamErr = e.ListPolicies(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "GetPolicy":
		var iamErr *iamError
		response, iamErr = e.GetPolicy(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListPolicyVersions":
		var iamErr *iamError
		response, iamErr = e.ListPolicyVersions(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "GetPolicyVersion":
		var iamErr *iamError
		response, iamErr = e.GetPolicyVersion(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "CreatePolicyVersion":
		var iamErr *iamError
		response, iamErr = e.CreatePolicyVersion(ctx, values)
		if iamErr != nil {
			glog.Errorf("CreatePolicyVersion: %+v", iamErr.Error)
			return nil, iamErr
		}
		changed = false
	case "DeletePolicyVersion":
		var iamErr *iamError
		response, iamErr = e.DeletePolicyVersion(ctx, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "SetUserStatus":
		var iamErr *iamError
		response, iamErr = e.SetUserStatus(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "UpdateAccessKey":
		var iamErr *iamError
		response, iamErr = e.UpdateAccessKey(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	// Service Account actions
	case "CreateServiceAccount":
		createdBy := values.Get("CreatedBy")
		var iamErr *iamError
		response, iamErr = e.CreateServiceAccount(s3cfg, values, createdBy)
		if iamErr != nil {
			return nil, iamErr
		}
	case "DeleteServiceAccount":
		var iamErr *iamError
		response, iamErr = e.DeleteServiceAccount(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "ListServiceAccounts":
		response = e.ListServiceAccounts(s3cfg, values)
		changed = false
	case "GetServiceAccount":
		var iamErr *iamError
		response, iamErr = e.GetServiceAccount(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "UpdateServiceAccount":
		var iamErr *iamError
		response, iamErr = e.UpdateServiceAccount(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	// Group actions
	case "CreateGroup":
		var iamErr *iamError
		response, iamErr = e.CreateGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "DeleteGroup":
		var iamErr *iamError
		response, iamErr = e.DeleteGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "UpdateGroup":
		var iamErr *iamError
		response, iamErr = e.UpdateGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "GetGroup":
		var iamErr *iamError
		response, iamErr = e.GetGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListGroups":
		response = e.ListGroups(s3cfg, values)
		changed = false
	case "AddUserToGroup":
		var iamErr *iamError
		response, iamErr = e.AddUserToGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "RemoveUserFromGroup":
		var iamErr *iamError
		response, iamErr = e.RemoveUserFromGroup(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "AttachGroupPolicy":
		var iamErr *iamError
		response, iamErr = e.AttachGroupPolicy(ctx, s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "DetachGroupPolicy":
		var iamErr *iamError
		response, iamErr = e.DetachGroupPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
	case "ListAttachedGroupPolicies":
		var iamErr *iamError
		response, iamErr = e.ListAttachedGroupPolicies(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "PutGroupPolicy":
		var iamErr *iamError
		response, iamErr = e.PutGroupPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "GetGroupPolicy":
		var iamErr *iamError
		response, iamErr = e.GetGroupPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "DeleteGroupPolicy":
		var iamErr *iamError
		response, iamErr = e.DeleteGroupPolicy(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListGroupPolicies":
		var iamErr *iamError
		response, iamErr = e.ListGroupPolicies(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	case "ListGroupsForUser":
		var iamErr *iamError
		response, iamErr = e.ListGroupsForUser(s3cfg, values)
		if iamErr != nil {
			return nil, iamErr
		}
		changed = false
	default:
		return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrNotImplemented).Code, Error: errors.New(s3err.GetAPIError(s3err.ErrNotImplemented).Description)}
	}
	if changed {
		if !skipPersist {
			if err := e.PutS3ApiConfiguration(s3cfg); err != nil {
				return nil, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
			}
		}
		// Reload in-memory identity maps so subsequent LookupByAccessKey calls
		// can see newly created or deleted keys immediately
		if err := e.ReloadConfiguration(); err != nil {
			glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
			// Don't fail the request since the persistent save succeeded
		}
	} else if action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "CreatePolicyVersion" || action == "DeletePolicy" || action == "CreateUser" || action == "PutGroupPolicy" || action == "DeleteGroupPolicy" {
		// Even if changed=false (persisted via credentialManager), we should still reload
		// if we are utilizing the local in-memory cache for speed
		if err := e.ReloadConfiguration(); err != nil {
			glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
		}
	}
	response.SetRequestId(reqID)
	return response, nil
}

// DoActions handles IAM API actions.
func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
	r, reqID := request_id.Ensure(r)
	if err := r.ParseForm(); err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}
	values := r.PostForm

	// Handle implicit username for HTTP requests
	switch r.Form.Get("Action") {
	case "ListAccessKeys", "CreateAccessKey", "DeleteAccessKey", "UpdateAccessKey", "ListUserPolicies", "GetUser":
		e.handleImplicitUsername(r, values)
	case "CreateServiceAccount":
		createdBy := s3_constants.GetIdentityNameFromContext(r)
		values.Set("CreatedBy", createdBy)
	}

	response, iamErr := e.ExecuteAction(r.Context(), values, false, reqID)
	if iamErr != nil {
		e.writeIamErrorResponse(w, r, reqID, iamErr)
		return
	}

	s3err.WriteXMLResponse(w, r, http.StatusOK, response)
}
