mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
7b44cf5627
* fix(iam): implement CreatePolicyVersion for managed policies The AWS Terraform provider updates a managed policy in place via CreatePolicyVersion, which returned 501 NotImplemented and broke terraform apply on any policy change. Implement CreatePolicyVersion (plus ListPolicyVersions, GetPolicyVersion and DeletePolicyVersion) on both the standalone IAM server and the embedded S3 IAM API. Managed policies keep a single current document, so each is modeled as one default version "v1": CreatePolicyVersion replaces the document, List/GetPolicyVersion expose it, and DeletePolicyVersion rejects deleting the default. GetPolicy now reports DefaultVersionId so the provider's read can fetch the document. The standalone path also refreshes the cached Identity.Actions of every identity the policy is attached to so the new document takes effect. * fix(iam): reject CreatePolicyVersion unless SetAsDefault=true With a single always-default managed-policy version, a request with SetAsDefault=false (or omitted) would stage a non-default version on AWS but here silently replaced the active document. Reject it on both the standalone and embedded paths. Isolate the new policy-version tests from the shared package fixtures so they stay order-independent, and assert IsDefaultVersion on the response.
660 lines
24 KiB
Go
660 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/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)
|
|
}
|
|
|
|
// 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.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)
|
|
}
|
|
}
|