iam: refuse DeleteUser while access keys remain

Deleting a user with live credentials orphaned its access keys; AWS
answers DeleteConflict until they are removed first.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Chris Lu
2026-10-06 09:25:32 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 3dcc74e93d
commit 85c706e015
4 changed files with 73 additions and 3 deletions
+13
View File
@@ -228,6 +228,19 @@ func (iama *IamApiServer) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName
resp = &DeleteUserResponse{}
for i, ident := range s3cfg.Identities {
if userName == ident.Name {
// AWS IAM behavior: prevent deletion if user has dependent 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)),
}
}
if len(ident.Credentials) > 0 {
return resp, &IamError{
Code: iam.ErrCodeDeleteConflictException,
Error: fmt.Errorf("cannot delete user %s: user has %d access key(s). Delete access keys first", userName, len(ident.Credentials)),
}
}
// Clean up any inline policies stored for this user
policies := Policies{}
if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) {
+23
View File
@@ -488,6 +488,29 @@ func TestDeleteUser(t *testing.T) {
out := DeleteUserResponse{}
response, err := executeRequest(req.HTTPRequest, out)
assert.Equal(t, nil, err)
assert.Equal(t, http.StatusConflict, response.Code)
listReq, _ := iam.New(session.New()).ListAccessKeysRequest(&iam.ListAccessKeysInput{UserName: userName})
_ = listReq.Build()
listOut := ListAccessKeysResponse{}
listResp, err := executeRequest(listReq.HTTPRequest, listOut)
assert.Equal(t, nil, err)
var listResult ListAccessKeysResponse
assert.Equal(t, nil, xml.Unmarshal(listResp.Body.Bytes(), &listResult))
for _, key := range listResult.ListAccessKeysResult.AccessKeyMetadata {
delKeyReq, _ := iam.New(session.New()).DeleteAccessKeyRequest(&iam.DeleteAccessKeyInput{
UserName: userName,
AccessKeyId: key.AccessKeyId,
})
_ = delKeyReq.Build()
_, err := executeRequest(delKeyReq.HTTPRequest, out)
assert.Equal(t, nil, err)
}
req, _ = iam.New(session.New()).DeleteUserRequest(params)
_ = req.Build()
response, err = executeRequest(req.HTTPRequest, out)
assert.Equal(t, nil, err)
assert.Equal(t, http.StatusOK, response.Code)
}
+9 -2
View File
@@ -355,8 +355,8 @@ func (e *EmbeddedIamApi) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName s
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
// AWS IAM behavior: prevent deletion if user has dependent resources.
// This ensures explicit cleanup and prevents orphaned resources.
if len(ident.ServiceAccountIds) > 0 {
return resp, &iamError{
Code: iam.ErrCodeDeleteConflictException,
@@ -364,6 +364,13 @@ func (e *EmbeddedIamApi) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName s
userName, len(ident.ServiceAccountIds)),
}
}
if len(ident.Credentials) > 0 {
return resp, &iamError{
Code: iam.ErrCodeDeleteConflictException,
Error: fmt.Errorf("cannot delete user %s: user has %d access key(s). Delete access keys first",
userName, len(ident.Credentials)),
}
}
s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...)
// Remove user from all groups
for _, g := range s3cfg.Groups {
+28 -1
View File
@@ -1692,7 +1692,34 @@ func TestEmbeddedIamFullWorkflow(t *testing.T) {
assert.Equal(t, http.StatusOK, response.Code)
})
// 5. Delete user
var accessKeyId string
for _, ident := range api.mockConfig.Identities {
if ident.Name == "WorkflowUser" && len(ident.Credentials) > 0 {
accessKeyId = ident.Credentials[0].AccessKey
}
}
assert.NotEmpty(t, accessKeyId)
// 5. Deleting a user that still has an access key is refused
t.Run("DeleteUserConflict", func(t *testing.T) {
params := &iam.DeleteUserInput{UserName: aws.String("WorkflowUser")}
req, _ := iam.New(session.New()).DeleteUserRequest(params)
_ = req.Build()
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusConflict, response.Code)
})
// 6. Delete the access key, then the user
t.Run("DeleteAccessKey", func(t *testing.T) {
params := &iam.DeleteAccessKeyInput{UserName: aws.String("WorkflowUser"), AccessKeyId: aws.String(accessKeyId)}
req, _ := iam.New(session.New()).DeleteAccessKeyRequest(params)
_ = req.Build()
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("DeleteUser", func(t *testing.T) {
params := &iam.DeleteUserInput{UserName: aws.String("WorkflowUser")}
req, _ := iam.New(session.New()).DeleteUserRequest(params)