Files
seaweedfs/weed/iamapi/iamapi_test.go
T
Chris Lu bb1ba8b247 iam: take the implicit user name from the verified identity (#11903)
* iam: take the implicit user name from the verified identity

For self-service actions sent without UserName, the implicit user was
resolved by re-parsing the raw Authorization header for a SigV4
credential and looking that access key up. The header is not what
authentication verified: a request can be authenticated by one
mechanism while carrying a header that names another user's access
key, and the action then ran against that other user.

The auth middleware already records the verified identity in the
request context, so use it, on both the standalone iam server and the
embedded IAM surface. When no verified identity is present the name is
left unset rather than guessed.

* iam: test implicit user name resolution

Cover a request authenticated by presigned credentials that also
carries a junk SigV4 header naming another user: the self-service
actions must run as the verified signer. Update the unit tests for the
context-based lookup.

* iam: resolve the implicit user for standalone GetUser too

The embedded surface fills UserName for GetUser; the standalone server
skipped it, so an implicit GetUser on `weed iam` answered NoSuchEntity.
Same verified-identity source as the other self-service actions.

* iam: cover all implicit-username actions in the regression test
2026-10-21 12:21:04 -07:00

674 lines
24 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/filer_pb"
"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/s3api/s3_constants"
"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)
}
// isolatedIamConfig is a self-contained IamS3ApiConfig backing whose state lives
// on the instance (not the package globals that executeRequest shares), so
// policy-version tests stay order-independent under -shuffle.
type isolatedIamConfig struct {
identities []*iam_pb.Identity
groups []*iam_pb.Group
policies Policies
}
func newIsolatedIamServer() *IamApiServer {
return &IamApiServer{s3ApiConfig: &isolatedIamConfig{
policies: Policies{Policies: make(map[string]policy_engine.PolicyDocument)},
}}
}
func (m *isolatedIamConfig) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
_ = copier.Copy(&s3cfg.Identities, &m.identities)
_ = copier.Copy(&s3cfg.Groups, &m.groups)
return nil
}
func (m *isolatedIamConfig) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
_ = copier.Copy(&m.identities, &s3cfg.Identities)
_ = copier.Copy(&m.groups, &s3cfg.Groups)
return nil
}
func (m *isolatedIamConfig) GetPolicies(policies *Policies) error {
_ = copier.Copy(&policies, &m.policies)
return nil
}
func (m *isolatedIamConfig) PutPolicies(policies *Policies) error {
_ = copier.Copy(&m.policies, &policies)
return nil
}
// TestCreatePolicyVersion reproduces issue #9785: the AWS Terraform provider
// updates a managed policy in place via CreatePolicyVersion, which previously
// returned 501 NotImplemented. The whole read/update surface Terraform relies on
// is exercised here: GetPolicy (DefaultVersionId), CreatePolicyVersion,
// GetPolicyVersion and ListPolicyVersions.
func TestCreatePolicyVersion(t *testing.T) {
srv := newIsolatedIamServer()
svc := iam.New(session.New())
policyName := "tf-managed-policy"
policyArn := aws.String("arn:aws:iam:::policy/" + policyName)
// Create the managed policy.
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*","s3:List*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeRequestWith(srv, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// Update it in place (what Terraform does). This used to return 501.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = cpvReq.Build()
resp, err = executeRequestWith(srv, cpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var cpvResp CreatePolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &cpvResp))
require.NotNil(t, cpvResp.CreatePolicyVersionResult.PolicyVersion.VersionId)
assert.Equal(t, "v1", *cpvResp.CreatePolicyVersionResult.PolicyVersion.VersionId)
require.NotNil(t, cpvResp.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
assert.True(t, *cpvResp.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
// GetPolicy must advertise a default version so Terraform's read can chain
// into GetPolicyVersion.
gpReq, _ := svc.GetPolicyRequest(&iam.GetPolicyInput{PolicyArn: policyArn})
_ = gpReq.Build()
resp, err = executeRequestWith(srv, gpReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpResp GetPolicyResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpResp))
require.NotNil(t, gpResp.GetPolicyResult.Policy.DefaultVersionId)
assert.Equal(t, "v1", *gpResp.GetPolicyResult.Policy.DefaultVersionId)
// GetPolicyVersion must return the updated document (Get* only, List* gone).
gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
_ = gpvReq.Build()
resp, err = executeRequestWith(srv, gpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpvResp GetPolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpvResp))
require.NotNil(t, gpvResp.GetPolicyVersionResult.PolicyVersion.Document)
assert.Contains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
assert.NotContains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:List*")
// ListPolicyVersions must report exactly the single default version.
lpvReq, _ := svc.ListPolicyVersionsRequest(&iam.ListPolicyVersionsInput{PolicyArn: policyArn})
_ = lpvReq.Build()
resp, err = executeRequestWith(srv, lpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var lpvResp ListPolicyVersionsResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &lpvResp))
require.Len(t, lpvResp.ListPolicyVersionsResult.Versions, 1)
assert.Equal(t, "v1", *lpvResp.ListPolicyVersionsResult.Versions[0].VersionId)
}
// TestCreatePolicyVersionMissingPolicy verifies a NoSuchEntity (404) when the
// target policy does not exist, matching AWS.
func TestCreatePolicyVersionMissingPolicy(t *testing.T) {
srv := newIsolatedIamServer()
req, _ := iam.New(session.New()).CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: aws.String("arn:aws:iam:::policy/does-not-exist"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
SetAsDefault: aws.Bool(true),
})
_ = req.Build()
resp, err := executeRequestWith(srv, req.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.Code)
code, _ := extractErrorCodeAndMessage(resp)
assert.Equal(t, "NoSuchEntity", code)
}
// TestCreatePolicyVersionRequiresSetAsDefault verifies that, given the
// single-version model, a request that does not set SetAsDefault=true is
// rejected rather than silently overwriting the active document.
func TestCreatePolicyVersionRequiresSetAsDefault(t *testing.T) {
srv := newIsolatedIamServer()
svc := iam.New(session.New())
policyArn := aws.String("arn:aws:iam:::policy/tf-managed-policy")
createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
PolicyName: aws.String("tf-managed-policy"),
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = createReq.Build()
resp, err := executeRequestWith(srv, createReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
// SetAsDefault omitted (defaults to false) must be rejected.
cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
PolicyArn: policyArn,
PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Put*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
})
_ = cpvReq.Build()
resp, err = executeRequestWith(srv, cpvReq.HTTPRequest, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.Code)
code, _ := extractErrorCodeAndMessage(resp)
assert.Equal(t, "InvalidInput", code)
// The live document must be untouched (still Get*, not Put*).
gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
_ = gpvReq.Build()
resp, err = executeRequestWith(srv, gpvReq.HTTPRequest, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
var gpvResp GetPolicyVersionResponse
require.NoError(t, xml.Unmarshal(resp.Body.Bytes(), &gpvResp))
require.NotNil(t, gpvResp.GetPolicyVersionResult.PolicyVersion.Document)
assert.Contains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
assert.NotContains(t, *gpvResp.GetPolicyVersionResult.PolicyVersion.Document, "s3:Put*")
}
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.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)
}
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) {
// The implicit name is the authenticated identity recorded in the request
// context by the auth middleware; request headers are never re-parsed.
iama := &IamApiServer{}
withIdentity := func(name string) *http.Request {
r := &http.Request{Header: http.Header{}}
return r.WithContext(s3_constants.SetIdentityNameInContext(r.Context(), name))
}
var tests = []struct {
r *http.Request
values url.Values
userName string
}{
// No authenticated identity - should not set username
{&http.Request{}, url.Values{}, ""},
// Authenticated identity in context - sets its name
{withIdentity("testuser1"), url.Values{}, "testuser1"},
// A client-controlled Authorization header naming another user is ignored
{&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{}, ""},
// An explicit UserName is left alone
{withIdentity("testuser1"), url.Values{"UserName": {"other"}}, "other"},
}
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
policies Policies
}
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
s3cfg.Groups = config.Groups
s3cfg.ServiceAccounts = config.ServiceAccounts
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 {
*policies = c.policies
if c.policies.Policies == nil && c.policies.InlinePolicies == nil {
return filer_pb.ErrNotFound
}
return nil
}
func (c *countingConfigSaver) PutPolicies(policies *Policies) error {
c.policies = *policies
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)
}
}