mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-17 07:11:30 +00:00
* fix(s3api,iamapi): avoid full SaveConfiguration when creating a single IAM user
When CreateUser was called, both the embedded IAM path (s3api_embedded_iam.go)
and the standalone IAM path (iamapi_management_handlers.go) would:
1. Load the full config (all N users)
2. Append the new user
3. Call SaveConfiguration, which re-writes ALL N user files in the filer_etc
store, triggering N file-change events and N reload cycles.
The fix replaces the full-save path with credentialManager.CreateUser, which
writes only the single new user's file. Set changed=false to skip the redundant
SaveConfiguration call, and add "CreateUser" to the reload-after-targeted-write
block so the in-memory config is refreshed.
Also adds a nil guard around iama.iam.GetCredentialManager() in DoActions to
avoid a nil-pointer panic in legacy test fixtures that leave iam unset.
Add SetCredentialManagerForTest to IdentityAccessManagement so tests can inject
an in-memory credential store without touching production code paths.
* test(s3api,iamapi): regression tests for CreateUser targeted-write fix
Add TestCreateUserDoesNotSaveAllUsers (iamapi) and
TestEmbeddedIamCreateUserDoesNotSaveAllUsers (s3api) to guard against the
regression where CreateUser would call SaveConfiguration and re-write all
N existing user files.
Both tests:
- Pre-populate 3 existing users
- Invoke CreateUser via the HTTP API
- Assert PutS3ApiConfiguration (full-config save) was NOT called
- Assert the new user is visible in the credential store
- Assert all pre-existing users are still intact
Also update TestEmbeddedIamExecuteAction to verify persistence via the
credential manager directly (mockConfig is no longer updated on CreateUser
since we skip the SaveConfiguration path).
* refactor(s3api,iamapi): share credential-error to IAM-code mapping
Move the credentialErrToIamErrCode helper from weed/iamapi to
weed/s3api as exported CredentialErrToIamErrCode and call it from
both the standalone IAM handler (iamapi) and the embedded IAM
handler (s3api). Previously the standalone path used the helper
while the embedded path duplicated the same switch inline; the two
sites could drift out of sync.
Also extend the mapping to cover ErrUserNotFound and
ErrAccessKeyNotFound (404 NoSuchEntity) so non-CreateUser callers
that opt into the helper get the right HTTP status.
* test(s3api): seed credential store explicitly in CreateUser regression
Previously the test relied on getS3ApiConfigurationFunc's syncOnce
side effect to populate the credential store with mockConfig
identities. If that fixture ever stopped seeding, the post-test
"pre-existing users still exist" assertion would silently start
passing for the wrong reason (the users were never created).
Seed cm.SaveConfiguration directly and assert the seed precondition
before the API call so any seeding regression fails loudly.
* fix(s3api): honor skipPersist in embedded CreateUser targeted path
ExecuteAction documents that skipPersist=true means "the changed
configuration is not saved to the persistent store", but the
targeted credentialManager.CreateUser added for the avoid-bulk-save
fix ran unconditionally. Gate it on !skipPersist so no-persist
callers don't silently leak a write to the credential store. Leave
changed=true in the skipPersist branch so the tail's existing
`if changed { if !skipPersist { ... } }` block keeps suppressing
persistence the same way it does for every other action.
Add TestEmbeddedIamCreateUserSkipPersist to pin the contract: a
CreateUser invocation with skipPersist=true must not call
PutS3ApiConfiguration and must not leave the user in the
credential store.
---------
Co-authored-by: Chris Lu <chris.lu@gmail.com>
490 lines
16 KiB
Go
490 lines
16 KiB
Go
package iamapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/gorilla/mux"
|
|
"github.com/jinzhu/copier"
|
|
"github.com/seaweedfs/seaweedfs/weed/credential"
|
|
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var GetS3ApiConfiguration func(s3cfg *iam_pb.S3ApiConfiguration) (err error)
|
|
var PutS3ApiConfiguration func(s3cfg *iam_pb.S3ApiConfiguration) (err error)
|
|
var GetPolicies func(policies *Policies) (err error)
|
|
var PutPolicies func(policies *Policies) (err error)
|
|
|
|
var s3config = iam_pb.S3ApiConfiguration{}
|
|
var policiesFile = Policies{Policies: make(map[string]policy_engine.PolicyDocument)}
|
|
var ias = IamApiServer{s3ApiConfig: iamS3ApiConfigureMock{}}
|
|
|
|
type iamS3ApiConfigureMock struct{}
|
|
|
|
func (iam iamS3ApiConfigureMock) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) {
|
|
_ = copier.Copy(&s3cfg.Identities, &s3config.Identities)
|
|
return nil
|
|
}
|
|
|
|
func (iam iamS3ApiConfigureMock) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) {
|
|
_ = copier.Copy(&s3config.Identities, &s3cfg.Identities)
|
|
return nil
|
|
}
|
|
|
|
func (iam iamS3ApiConfigureMock) GetPolicies(policies *Policies) (err error) {
|
|
_ = copier.Copy(&policies, &policiesFile)
|
|
return nil
|
|
}
|
|
|
|
func (iam iamS3ApiConfigureMock) PutPolicies(policies *Policies) (err error) {
|
|
_ = copier.Copy(&policiesFile, &policies)
|
|
return nil
|
|
}
|
|
|
|
func TestCreateUser(t *testing.T) {
|
|
userName := aws.String("Test")
|
|
params := &iam.CreateUserInput{UserName: userName}
|
|
req, _ := iam.New(session.New()).CreateUserRequest(params)
|
|
_ = req.Build()
|
|
out := CreateUserResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
//assert.Equal(t, out.XMLName, "lol")
|
|
}
|
|
|
|
func TestListUsers(t *testing.T) {
|
|
params := &iam.ListUsersInput{}
|
|
req, _ := iam.New(session.New()).ListUsersRequest(params)
|
|
_ = req.Build()
|
|
out := ListUsersResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestListUsersRequestIdMatchesResponseHeader(t *testing.T) {
|
|
params := &iam.ListUsersInput{}
|
|
req, _ := iam.New(session.New()).ListUsersRequest(params)
|
|
_ = req.Build()
|
|
|
|
out := ListUsersResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
|
|
headerRequestID := response.Header().Get(request_id.AmzRequestIDHeader)
|
|
assert.NotEmpty(t, headerRequestID)
|
|
assert.Equal(t, headerRequestID, extractRequestID(response))
|
|
}
|
|
|
|
func TestListAccessKeys(t *testing.T) {
|
|
svc := iam.New(session.New())
|
|
params := &iam.ListAccessKeysInput{}
|
|
req, _ := svc.ListAccessKeysRequest(params)
|
|
_ = req.Build()
|
|
out := ListAccessKeysResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestUpdateAccessKey(t *testing.T) {
|
|
svc := iam.New(session.New())
|
|
|
|
createReq, _ := svc.CreateAccessKeyRequest(&iam.CreateAccessKeyInput{UserName: aws.String("Test")})
|
|
_ = createReq.Build()
|
|
createOut := CreateAccessKeyResponse{}
|
|
response, err := executeRequest(createReq.HTTPRequest, createOut)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
|
|
var createResp CreateAccessKeyResponse
|
|
err = xml.Unmarshal(response.Body.Bytes(), &createResp)
|
|
assert.Equal(t, nil, err)
|
|
accessKeyId := createResp.CreateAccessKeyResult.AccessKey.AccessKeyId
|
|
if accessKeyId == nil {
|
|
t.Fatalf("expected access key id to be set")
|
|
}
|
|
|
|
updateReq, _ := svc.UpdateAccessKeyRequest(&iam.UpdateAccessKeyInput{
|
|
UserName: aws.String("Test"),
|
|
AccessKeyId: accessKeyId,
|
|
Status: aws.String("Inactive"),
|
|
})
|
|
_ = updateReq.Build()
|
|
updateOut := UpdateAccessKeyResponse{}
|
|
response, err = executeRequest(updateReq.HTTPRequest, updateOut)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
|
|
listReq, _ := svc.ListAccessKeysRequest(&iam.ListAccessKeysInput{UserName: aws.String("Test")})
|
|
_ = listReq.Build()
|
|
listOut := ListAccessKeysResponse{}
|
|
response, err = executeRequest(listReq.HTTPRequest, listOut)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
|
|
var listResp ListAccessKeysResponse
|
|
err = xml.Unmarshal(response.Body.Bytes(), &listResp)
|
|
assert.Equal(t, nil, err)
|
|
found := false
|
|
for _, key := range listResp.ListAccessKeysResult.AccessKeyMetadata {
|
|
if key.AccessKeyId != nil && *key.AccessKeyId == *accessKeyId {
|
|
found = true
|
|
if assert.NotNil(t, key.Status) {
|
|
assert.Equal(t, "Inactive", *key.Status)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestGetUser(t *testing.T) {
|
|
userName := aws.String("Test")
|
|
params := &iam.GetUserInput{UserName: userName}
|
|
req, _ := iam.New(session.New()).GetUserRequest(params)
|
|
_ = req.Build()
|
|
out := GetUserResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
// Todo flat statement
|
|
func TestCreatePolicy(t *testing.T) {
|
|
params := &iam.CreatePolicyInput{
|
|
PolicyName: aws.String("S3-read-only-example-bucket"),
|
|
PolicyDocument: aws.String(`
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:Get*",
|
|
"s3:List*"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::EXAMPLE-BUCKET",
|
|
"arn:aws:s3:::EXAMPLE-BUCKET/*"
|
|
]
|
|
}
|
|
]
|
|
}`),
|
|
}
|
|
req, _ := iam.New(session.New()).CreatePolicyRequest(params)
|
|
_ = req.Build()
|
|
out := CreatePolicyResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestPutUserPolicy(t *testing.T) {
|
|
userName := aws.String("Test")
|
|
params := &iam.PutUserPolicyInput{
|
|
UserName: userName,
|
|
PolicyName: aws.String("S3-read-only-example-bucket"),
|
|
PolicyDocument: aws.String(
|
|
`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:Get*",
|
|
"s3:List*"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::EXAMPLE-BUCKET",
|
|
"arn:aws:s3:::EXAMPLE-BUCKET/*"
|
|
]
|
|
}
|
|
]
|
|
}`),
|
|
}
|
|
req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
out := PutUserPolicyResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestPutUserPolicyError(t *testing.T) {
|
|
userName := aws.String("InvalidUser")
|
|
params := &iam.PutUserPolicyInput{
|
|
UserName: userName,
|
|
PolicyName: aws.String("S3-read-only-example-bucket"),
|
|
PolicyDocument: aws.String(
|
|
`{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:Get*",
|
|
"s3:List*"
|
|
],
|
|
"Resource": [
|
|
"arn:aws:s3:::EXAMPLE-BUCKET",
|
|
"arn:aws:s3:::EXAMPLE-BUCKET/*"
|
|
]
|
|
}
|
|
]
|
|
}`),
|
|
}
|
|
req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
response, err := executeRequest(req.HTTPRequest, nil)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusNotFound, response.Code)
|
|
|
|
expectedMessage := "the user with name InvalidUser cannot be found"
|
|
expectedCode := "NoSuchEntity"
|
|
|
|
code, message := extractErrorCodeAndMessage(response)
|
|
|
|
assert.Equal(t, expectedMessage, message)
|
|
assert.Equal(t, expectedCode, code)
|
|
assert.Contains(t, response.Body.String(), "<RequestId>")
|
|
assert.NotContains(t, response.Body.String(), "<ResponseMetadata>")
|
|
assert.Equal(t, response.Header().Get(request_id.AmzRequestIDHeader), extractRequestID(response))
|
|
}
|
|
|
|
func extractErrorCodeAndMessage(response *httptest.ResponseRecorder) (string, string) {
|
|
pattern := `<Error><Code>(.*)</Code><Message>(.*)</Message><Type>(.*)</Type></Error>`
|
|
re := regexp.MustCompile(pattern)
|
|
|
|
code := re.FindStringSubmatch(response.Body.String())[1]
|
|
message := re.FindStringSubmatch(response.Body.String())[2]
|
|
return code, message
|
|
}
|
|
|
|
func extractRequestID(response *httptest.ResponseRecorder) string {
|
|
re := regexp.MustCompile(`<RequestId>([^<]+)</RequestId>`)
|
|
matches := re.FindStringSubmatch(response.Body.String())
|
|
if len(matches) < 2 {
|
|
return ""
|
|
}
|
|
return matches[1]
|
|
}
|
|
|
|
func TestGetUserPolicy(t *testing.T) {
|
|
userName := aws.String("Test")
|
|
params := &iam.GetUserPolicyInput{UserName: userName, PolicyName: aws.String("S3-read-only-example-bucket")}
|
|
req, _ := iam.New(session.New()).GetUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
out := GetUserPolicyResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestUpdateUser(t *testing.T) {
|
|
userName := aws.String("Test")
|
|
newUserName := aws.String("Test-New")
|
|
params := &iam.UpdateUserInput{NewUserName: newUserName, UserName: userName}
|
|
req, _ := iam.New(session.New()).UpdateUserRequest(params)
|
|
_ = req.Build()
|
|
out := UpdateUserResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func TestDeleteUser(t *testing.T) {
|
|
userName := aws.String("Test-New")
|
|
params := &iam.DeleteUserInput{UserName: userName}
|
|
req, _ := iam.New(session.New()).DeleteUserRequest(params)
|
|
_ = req.Build()
|
|
out := DeleteUserResponse{}
|
|
response, err := executeRequest(req.HTTPRequest, out)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
}
|
|
|
|
func executeRequest(req *http.Request, v interface{}) (*httptest.ResponseRecorder, error) {
|
|
rr := httptest.NewRecorder()
|
|
apiRouter := mux.NewRouter().SkipClean(true)
|
|
apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(ias.DoActions)
|
|
apiRouter.ServeHTTP(rr, req)
|
|
return rr, xml.Unmarshal(rr.Body.Bytes(), &v)
|
|
}
|
|
|
|
func TestHandleImplicitUsername(t *testing.T) {
|
|
// Create a mock IamApiServer with credential store
|
|
// The handleImplicitUsername function now looks up the username from the
|
|
// credential store based on AccessKeyId, not from the region field in the auth header.
|
|
// Note: Using obviously fake access keys to avoid CI secret scanner false positives
|
|
|
|
// Create IAM directly as struct literal (same pattern as other tests)
|
|
iam := &s3api.IdentityAccessManagement{}
|
|
|
|
// Load test credentials - map access key to identity name
|
|
testConfig := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "testuser1",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: "AKIATESTFAKEKEY000001", SecretKey: "testsecretfake"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(t, testConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load test config: %v", err)
|
|
}
|
|
|
|
iama := &IamApiServer{
|
|
iam: iam,
|
|
}
|
|
|
|
var tests = []struct {
|
|
r *http.Request
|
|
values url.Values
|
|
userName string
|
|
}{
|
|
// No authorization header - should not set username
|
|
{&http.Request{}, url.Values{}, ""},
|
|
// Valid auth header with known access key - should look up and find "testuser1"
|
|
{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIATESTFAKEKEY000001/20220420/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, "testuser1"},
|
|
// Malformed auth header (no Credential=) - should not set username
|
|
{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 =AKIATESTFAKEKEY000001/20220420/test1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, ""},
|
|
// Unknown access key - should not set username
|
|
{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIATESTUNKNOWN000000/20220420/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, ""},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
iama.handleImplicitUsername(test.r, test.values)
|
|
if un := test.values.Get("UserName"); un != test.userName {
|
|
t.Errorf("No.%d: Got: %v, Expected: %v", i, un, test.userName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustMarshalJSON(t *testing.T, v interface{}) []byte {
|
|
t.Helper()
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal JSON: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// iamApiServerWithCredentialManager builds an IamApiServer backed by a real
|
|
// in-memory credential store. Used for tests that exercise the targeted
|
|
// credentialManager paths (e.g. CreateUser) instead of the legacy mock.
|
|
func iamApiServerWithCredentialManager() (*IamApiServer, *credential.CredentialManager, *countingConfigSaver) {
|
|
store := &memory.MemoryStore{}
|
|
store.Initialize(nil, "")
|
|
cm := &credential.CredentialManager{Store: store}
|
|
|
|
counter := &countingConfigSaver{cm: cm}
|
|
iamInstance := &s3api.IdentityAccessManagement{}
|
|
// Wire up the credential manager so GetCredentialManager() returns it.
|
|
iamInstance.SetCredentialManagerForTest(cm)
|
|
|
|
srv := &IamApiServer{
|
|
s3ApiConfig: counter,
|
|
iam: iamInstance,
|
|
}
|
|
return srv, cm, counter
|
|
}
|
|
|
|
// countingConfigSaver wraps the credential manager and counts full-config saves.
|
|
type countingConfigSaver struct {
|
|
cm *credential.CredentialManager
|
|
putCalled int
|
|
}
|
|
|
|
func (c *countingConfigSaver) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
config, err := c.cm.LoadConfiguration(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s3cfg.Identities = config.Identities
|
|
s3cfg.Policies = config.Policies
|
|
return nil
|
|
}
|
|
|
|
func (c *countingConfigSaver) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
c.putCalled++
|
|
return c.cm.SaveConfiguration(context.Background(), s3cfg)
|
|
}
|
|
|
|
func (c *countingConfigSaver) GetPolicies(policies *Policies) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *countingConfigSaver) PutPolicies(policies *Policies) error {
|
|
return nil
|
|
}
|
|
|
|
func executeRequestWith(srv *IamApiServer, req *http.Request, v interface{}) (*httptest.ResponseRecorder, error) {
|
|
rr := httptest.NewRecorder()
|
|
apiRouter := mux.NewRouter().SkipClean(true)
|
|
apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(srv.DoActions)
|
|
apiRouter.ServeHTTP(rr, req)
|
|
return rr, xml.Unmarshal(rr.Body.Bytes(), &v)
|
|
}
|
|
|
|
// TestCreateUserDoesNotSaveAllUsers is a regression test for the bug where
|
|
// creating a new user triggered a full SaveConfiguration over all existing
|
|
// identities (causing N file writes + reload cycles in the filer_etc store).
|
|
// The fix uses credentialManager.CreateUser for a targeted single-file write.
|
|
func TestCreateUserDoesNotSaveAllUsers(t *testing.T) {
|
|
srv, cm, counter := iamApiServerWithCredentialManager()
|
|
ctx := context.Background()
|
|
|
|
// Pre-populate three existing users.
|
|
for _, name := range []string{"existing-1", "existing-2", "existing-3"} {
|
|
require.NoError(t, cm.CreateUser(ctx, &iam_pb.Identity{Name: name}))
|
|
}
|
|
|
|
// Create a new user via the HTTP API.
|
|
params := &iam.CreateUserInput{UserName: aws.String("new-user")}
|
|
req, _ := iam.New(session.New()).CreateUserRequest(params)
|
|
_ = req.Build()
|
|
out := CreateUserResponse{}
|
|
resp, err := executeRequestWith(srv, req.HTTPRequest, &out)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.Code)
|
|
|
|
// The new user must appear in the store.
|
|
newUser, userErr := cm.GetUser(ctx, "new-user")
|
|
require.NoError(t, userErr)
|
|
require.Equal(t, "new-user", newUser.Name)
|
|
|
|
// Critical: full SaveConfiguration (PutS3ApiConfiguration) must NOT have
|
|
// been called. Before the fix it was called once per CreateUser, rewriting
|
|
// every existing user file and triggering a cascade of reload events.
|
|
assert.Equal(t, 0, counter.putCalled,
|
|
"CreateUser must not trigger a full PutS3ApiConfiguration over all users")
|
|
|
|
// All pre-existing users must still be intact.
|
|
for _, name := range []string{"existing-1", "existing-2", "existing-3"} {
|
|
u, err := cm.GetUser(ctx, name)
|
|
require.NoError(t, err, "pre-existing user %s should still exist", name)
|
|
assert.Equal(t, name, u.Name)
|
|
}
|
|
}
|