aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_embedded_iam_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3api_embedded_iam_test.go')
-rw-r--r--weed/s3api/s3api_embedded_iam_test.go1028
1 files changed, 1028 insertions, 0 deletions
diff --git a/weed/s3api/s3api_embedded_iam_test.go b/weed/s3api/s3api_embedded_iam_test.go
new file mode 100644
index 000000000..81839084b
--- /dev/null
+++ b/weed/s3api/s3api_embedded_iam_test.go
@@ -0,0 +1,1028 @@
+package s3api
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "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/seaweedfs/seaweedfs/weed/pb/iam_pb"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/proto"
+)
+
+// EmbeddedIamApiForTest is a testable version of EmbeddedIamApi
+type EmbeddedIamApiForTest struct {
+ *EmbeddedIamApi
+ mockConfig *iam_pb.S3ApiConfiguration
+}
+
+func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
+ e := &EmbeddedIamApiForTest{
+ EmbeddedIamApi: &EmbeddedIamApi{
+ iam: &IdentityAccessManagement{},
+ },
+ mockConfig: &iam_pb.S3ApiConfiguration{},
+ }
+ return e
+}
+
+// Override GetS3ApiConfiguration for testing
+func (e *EmbeddedIamApiForTest) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
+ // Use proto.Clone for proper deep copy semantics
+ if e.mockConfig != nil {
+ cloned := proto.Clone(e.mockConfig).(*iam_pb.S3ApiConfiguration)
+ proto.Merge(s3cfg, cloned)
+ }
+ return nil
+}
+
+// Override PutS3ApiConfiguration for testing
+func (e *EmbeddedIamApiForTest) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
+ // Use proto.Clone for proper deep copy semantics
+ e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
+ return nil
+}
+
+// DoActions handles IAM API actions for testing
+func (e *EmbeddedIamApiForTest) DoActions(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Invalid request", http.StatusBadRequest)
+ return
+ }
+ values := r.PostForm
+ s3cfg := &iam_pb.S3ApiConfiguration{}
+ if err := e.GetS3ApiConfiguration(s3cfg); err != nil {
+ http.Error(w, "Internal error", http.StatusInternalServerError)
+ return
+ }
+
+ var response interface{}
+ var iamErr *iamError
+ changed := true
+
+ switch r.Form.Get("Action") {
+ case "ListUsers":
+ response = e.ListUsers(s3cfg, values)
+ changed = false
+ case "ListAccessKeys":
+ e.handleImplicitUsername(r, values)
+ response = e.ListAccessKeys(s3cfg, values)
+ changed = false
+ case "CreateUser":
+ response, iamErr = e.CreateUser(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ case "GetUser":
+ userName := values.Get("UserName")
+ response, iamErr = e.GetUser(s3cfg, userName)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ changed = false
+ case "UpdateUser":
+ response, iamErr = e.UpdateUser(s3cfg, values)
+ if iamErr != nil {
+ http.Error(w, "Invalid request", http.StatusBadRequest)
+ return
+ }
+ case "DeleteUser":
+ userName := values.Get("UserName")
+ response, iamErr = e.DeleteUser(s3cfg, userName)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ case "CreateAccessKey":
+ e.handleImplicitUsername(r, values)
+ response, iamErr = e.CreateAccessKey(s3cfg, values)
+ if iamErr != nil {
+ http.Error(w, "Internal error", http.StatusInternalServerError)
+ return
+ }
+ case "DeleteAccessKey":
+ e.handleImplicitUsername(r, values)
+ response = e.DeleteAccessKey(s3cfg, values)
+ case "CreatePolicy":
+ response, iamErr = e.CreatePolicy(s3cfg, values)
+ if iamErr != nil {
+ http.Error(w, "Invalid request", http.StatusBadRequest)
+ return
+ }
+ case "PutUserPolicy":
+ response, iamErr = e.PutUserPolicy(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ case "GetUserPolicy":
+ response, iamErr = e.GetUserPolicy(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ changed = false
+ case "DeleteUserPolicy":
+ response, iamErr = e.DeleteUserPolicy(s3cfg, values)
+ if iamErr != nil {
+ e.writeIamErrorResponse(w, r, iamErr)
+ return
+ }
+ default:
+ http.Error(w, "Not implemented", http.StatusNotImplemented)
+ return
+ }
+
+ if changed {
+ if err := e.PutS3ApiConfiguration(s3cfg); err != nil {
+ http.Error(w, "Internal error", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/xml")
+ w.WriteHeader(http.StatusOK)
+ xmlBytes, err := xml.Marshal(response)
+ if err != nil {
+ // This should not happen in tests, but log it for debugging
+ http.Error(w, "Internal error: failed to marshal response", http.StatusInternalServerError)
+ return
+ }
+ _, _ = w.Write(xmlBytes)
+}
+
+// executeEmbeddedIamRequest executes an IAM request against the given API instance.
+// If v is non-nil, the response body is unmarshalled into it.
+func executeEmbeddedIamRequest(api *EmbeddedIamApiForTest, req *http.Request, v interface{}) (*httptest.ResponseRecorder, error) {
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+ if v != nil {
+ if err := xml.Unmarshal(rr.Body.Bytes(), v); err != nil {
+ return rr, err
+ }
+ }
+ return rr, nil
+}
+
+// embeddedIamErrorResponseForTest is used for parsing IAM error responses in tests
+type embeddedIamErrorResponseForTest struct {
+ Error struct {
+ Code string `xml:"Code"`
+ Message string `xml:"Message"`
+ } `xml:"Error"`
+}
+
+func extractEmbeddedIamErrorCodeAndMessage(response *httptest.ResponseRecorder) (string, string) {
+ var er embeddedIamErrorResponseForTest
+ if err := xml.Unmarshal(response.Body.Bytes(), &er); err != nil {
+ return "", ""
+ }
+ return er.Error.Code, er.Error.Message
+}
+
+// TestEmbeddedIamCreateUser tests creating a user via the embedded IAM API
+func TestEmbeddedIamCreateUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ userName := aws.String("TestUser")
+ params := &iam.CreateUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).CreateUserRequest(params)
+ _ = req.Build()
+ out := iamCreateUserResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify response contains correct username
+ assert.NotNil(t, out.CreateUserResult.User.UserName)
+ assert.Equal(t, "TestUser", *out.CreateUserResult.User.UserName)
+
+ // Verify user was persisted in config
+ assert.Len(t, api.mockConfig.Identities, 1)
+ assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)
+}
+
+// TestEmbeddedIamListUsers tests listing users via the embedded IAM API
+func TestEmbeddedIamListUsers(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "User1"},
+ {Name: "User2"},
+ },
+ }
+
+ params := &iam.ListUsersInput{}
+ req, _ := iam.New(session.New()).ListUsersRequest(params)
+ _ = req.Build()
+ out := iamListUsersResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify response contains the users
+ assert.Len(t, out.ListUsersResult.Users, 2)
+}
+
+// TestEmbeddedIamListAccessKeys tests listing access keys via the embedded IAM API
+func TestEmbeddedIamListAccessKeys(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ svc := iam.New(session.New())
+ params := &iam.ListAccessKeysInput{}
+ req, _ := svc.ListAccessKeysRequest(params)
+ _ = req.Build()
+ out := iamListAccessKeysResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+}
+
+// TestEmbeddedIamGetUser tests getting a user via the embedded IAM API
+func TestEmbeddedIamGetUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "TestUser"},
+ },
+ }
+
+ userName := aws.String("TestUser")
+ params := &iam.GetUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).GetUserRequest(params)
+ _ = req.Build()
+ out := iamGetUserResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify response contains correct username
+ assert.NotNil(t, out.GetUserResult.User.UserName)
+ assert.Equal(t, "TestUser", *out.GetUserResult.User.UserName)
+}
+
+// TestEmbeddedIamCreatePolicy tests creating a policy via the embedded IAM API
+func TestEmbeddedIamCreatePolicy(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ 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 := iamCreatePolicyResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify response contains policy metadata
+ assert.NotNil(t, out.CreatePolicyResult.Policy.PolicyName)
+ assert.Equal(t, "S3-read-only-example-bucket", *out.CreatePolicyResult.Policy.PolicyName)
+ assert.NotNil(t, out.CreatePolicyResult.Policy.Arn)
+ assert.NotNil(t, out.CreatePolicyResult.Policy.PolicyId)
+}
+
+// TestEmbeddedIamPutUserPolicy tests attaching a policy to a user
+func TestEmbeddedIamPutUserPolicy(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "TestUser"},
+ },
+ }
+
+ userName := aws.String("TestUser")
+ 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 := iamPutUserPolicyResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify policy was attached to the user (actions should be set)
+ assert.Len(t, api.mockConfig.Identities, 1)
+ assert.NotEmpty(t, api.mockConfig.Identities[0].Actions)
+}
+
+// TestEmbeddedIamPutUserPolicyError tests error handling when user doesn't exist
+func TestEmbeddedIamPutUserPolicyError(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ 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 := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusNotFound, response.Code)
+
+ expectedCode := "NoSuchEntity"
+ code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
+ assert.Equal(t, expectedCode, code)
+}
+
+// TestEmbeddedIamGetUserPolicy tests getting a user's policy
+func TestEmbeddedIamGetUserPolicy(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {
+ Name: "TestUser",
+ Actions: []string{"Read", "List"},
+ },
+ },
+ }
+
+ userName := aws.String("TestUser")
+ params := &iam.GetUserPolicyInput{
+ UserName: userName,
+ PolicyName: aws.String("S3-read-only-example-bucket"),
+ }
+ req, _ := iam.New(session.New()).GetUserPolicyRequest(params)
+ _ = req.Build()
+ out := iamGetUserPolicyResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+}
+
+// TestEmbeddedIamDeleteUserPolicy tests deleting a user's policy (clears actions)
+func TestEmbeddedIamDeleteUserPolicy(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {
+ Name: "TestUser",
+ Actions: []string{"Read", "Write", "List"},
+ Credentials: []*iam_pb.Credential{
+ {AccessKey: "AKIATEST12345", SecretKey: "secret"},
+ },
+ },
+ },
+ }
+
+ // Use direct form post for DeleteUserPolicy
+ form := url.Values{}
+ form.Set("Action", "DeleteUserPolicy")
+ form.Set("UserName", "TestUser")
+ form.Set("PolicyName", "TestPolicy")
+
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.PostForm = form
+ req.Form = form
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+
+ assert.Equal(t, http.StatusOK, rr.Code)
+
+ // CRITICAL: Verify user still exists (was NOT deleted)
+ assert.Len(t, api.mockConfig.Identities, 1, "User should NOT be deleted")
+ assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)
+
+ // Verify credentials are still intact
+ assert.Len(t, api.mockConfig.Identities[0].Credentials, 1, "Credentials should NOT be deleted")
+ assert.Equal(t, "AKIATEST12345", api.mockConfig.Identities[0].Credentials[0].AccessKey)
+
+ // Verify actions/policy was cleared
+ assert.Nil(t, api.mockConfig.Identities[0].Actions, "Actions should be cleared")
+}
+
+// TestEmbeddedIamDeleteUserPolicyUserNotFound tests error when user doesn't exist
+func TestEmbeddedIamDeleteUserPolicyUserNotFound(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ form := url.Values{}
+ form.Set("Action", "DeleteUserPolicy")
+ form.Set("UserName", "NonExistentUser")
+ form.Set("PolicyName", "TestPolicy")
+
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.PostForm = form
+ req.Form = form
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+
+ assert.Equal(t, http.StatusNotFound, rr.Code)
+}
+
+// TestEmbeddedIamUpdateUser tests updating a user
+func TestEmbeddedIamUpdateUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "TestUser"},
+ },
+ }
+
+ userName := aws.String("TestUser")
+ newUserName := aws.String("TestUser-New")
+ params := &iam.UpdateUserInput{NewUserName: newUserName, UserName: userName}
+ req, _ := iam.New(session.New()).UpdateUserRequest(params)
+ _ = req.Build()
+ out := iamUpdateUserResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+}
+
+// TestEmbeddedIamDeleteUser tests deleting a user
+func TestEmbeddedIamDeleteUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "TestUser-New"},
+ },
+ }
+
+ userName := aws.String("TestUser-New")
+ params := &iam.DeleteUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).DeleteUserRequest(params)
+ _ = req.Build()
+ out := iamDeleteUserResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+}
+
+// TestEmbeddedIamCreateAccessKey tests creating an access key
+func TestEmbeddedIamCreateAccessKey(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "TestUser"},
+ },
+ }
+
+ userName := aws.String("TestUser")
+ params := &iam.CreateAccessKeyInput{UserName: userName}
+ req, _ := iam.New(session.New()).CreateAccessKeyRequest(params)
+ _ = req.Build()
+ out := iamCreateAccessKeyResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify response contains access key credentials
+ assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.AccessKeyId)
+ assert.NotEmpty(t, *out.CreateAccessKeyResult.AccessKey.AccessKeyId)
+ assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
+ assert.NotEmpty(t, *out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
+ assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.UserName)
+ assert.Equal(t, "TestUser", *out.CreateAccessKeyResult.AccessKey.UserName)
+
+ // Verify credentials were persisted
+ assert.Len(t, api.mockConfig.Identities[0].Credentials, 1)
+}
+
+// TestEmbeddedIamDeleteAccessKey tests deleting an access key via direct form post
+func TestEmbeddedIamDeleteAccessKey(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {
+ Name: "TestUser",
+ Credentials: []*iam_pb.Credential{
+ {AccessKey: "AKIATEST12345", SecretKey: "secret"},
+ },
+ },
+ },
+ }
+
+ // Use direct form post since AWS SDK may format differently
+ form := url.Values{}
+ form.Set("Action", "DeleteAccessKey")
+ form.Set("UserName", "TestUser")
+ form.Set("AccessKeyId", "AKIATEST12345")
+
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.PostForm = form
+ req.Form = form
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+
+ assert.Equal(t, http.StatusOK, rr.Code)
+
+ // Verify the access key was deleted
+ assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
+}
+
+// TestEmbeddedIamHandleImplicitUsername tests implicit username extraction from authorization header
+func TestEmbeddedIamHandleImplicitUsername(t *testing.T) {
+ // Create IAM with test credentials - the handleImplicitUsername function now looks
+ // up the username from the credential store based on AccessKeyId
+ // Note: Using obviously fake access keys to avoid secret scanner false positives
+ iam := &IdentityAccessManagement{}
+ testConfig := &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {
+ Name: "testuser1",
+ Credentials: []*iam_pb.Credential{
+ {AccessKey: "AKIATESTFAKEKEY000001", SecretKey: "testsecretfake"},
+ },
+ },
+ },
+ }
+ err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
+ if err != nil {
+ t.Fatalf("Failed to load test config: %v", err)
+ }
+
+ embeddedApi := &EmbeddedIamApi{
+ 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 {
+ embeddedApi.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(v interface{}) []byte {
+ data, err := json.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ return data
+}
+
+// TestEmbeddedIamFullWorkflow tests a complete user lifecycle
+func TestEmbeddedIamFullWorkflow(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ // 1. Create user
+ t.Run("CreateUser", func(t *testing.T) {
+ userName := aws.String("WorkflowUser")
+ params := &iam.CreateUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).CreateUserRequest(params)
+ _ = req.Build()
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+ })
+
+ // 2. Create access key for user
+ t.Run("CreateAccessKey", func(t *testing.T) {
+ userName := aws.String("WorkflowUser")
+ params := &iam.CreateAccessKeyInput{UserName: userName}
+ req, _ := iam.New(session.New()).CreateAccessKeyRequest(params)
+ _ = req.Build()
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+ })
+
+ // 3. Attach policy to user
+ t.Run("PutUserPolicy", func(t *testing.T) {
+ params := &iam.PutUserPolicyInput{
+ UserName: aws.String("WorkflowUser"),
+ PolicyName: aws.String("ReadWritePolicy"),
+ PolicyDocument: aws.String(`{
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Action": ["s3:Get*", "s3:Put*"],
+ "Resource": ["arn:aws:s3:::*"]
+ }]
+ }`),
+ }
+ req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
+ _ = req.Build()
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+ })
+
+ // 4. List users to verify
+ t.Run("ListUsers", func(t *testing.T) {
+ params := &iam.ListUsersInput{}
+ req, _ := iam.New(session.New()).ListUsersRequest(params)
+ _ = req.Build()
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+ })
+
+ // 5. Delete user
+ t.Run("DeleteUser", 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.StatusOK, response.Code)
+ })
+}
+
+// TestIamStringSlicesEqual tests the iamStringSlicesEqual helper function
+func TestIamStringSlicesEqual(t *testing.T) {
+ tests := []struct {
+ name string
+ a []string
+ b []string
+ expected bool
+ }{
+ {"both empty", []string{}, []string{}, true},
+ {"both nil", nil, nil, true},
+ {"same elements same order", []string{"a", "b", "c"}, []string{"a", "b", "c"}, true},
+ {"same elements different order", []string{"c", "a", "b"}, []string{"a", "b", "c"}, true},
+ {"different lengths", []string{"a", "b"}, []string{"a", "b", "c"}, false},
+ {"different elements", []string{"a", "b", "c"}, []string{"a", "b", "d"}, false},
+ {"one empty one not", []string{}, []string{"a"}, false},
+ {"duplicates same", []string{"a", "a", "b"}, []string{"a", "b", "a"}, true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := iamStringSlicesEqual(tt.a, tt.b)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+// TestIamHash tests the iamHash function
+func TestIamHash(t *testing.T) {
+ input := "test-policy-document"
+ hash := iamHash(&input)
+
+ // Hash should be non-empty
+ assert.NotEmpty(t, hash)
+
+ // Same input should produce same hash
+ hash2 := iamHash(&input)
+ assert.Equal(t, hash, hash2)
+
+ // Different input should produce different hash
+ input2 := "different-policy"
+ hash3 := iamHash(&input2)
+ assert.NotEqual(t, hash, hash3)
+}
+
+// TestIamStringWithCharset tests the cryptographically secure random string generator
+func TestIamStringWithCharset(t *testing.T) {
+ charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ length := 20
+
+ str, err := iamStringWithCharset(length, charset)
+ assert.NoError(t, err)
+ assert.Len(t, str, length)
+
+ // All characters should be from the charset
+ for _, c := range str {
+ assert.Contains(t, charset, string(c))
+ }
+
+ // Two calls should produce different strings (with very high probability)
+ str2, err := iamStringWithCharset(length, charset)
+ assert.NoError(t, err)
+ assert.NotEqual(t, str, str2)
+}
+
+// TestIamMapToStatementAction tests action mapping
+func TestIamMapToStatementAction(t *testing.T) {
+ // iamMapToStatementAction maps IAM statement action patterns to internal action names
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {"*", "Admin"},
+ {"Get*", "Read"},
+ {"Put*", "Write"},
+ {"List*", "List"},
+ {"Tagging*", "Tagging"},
+ {"DeleteBucket*", "DeleteBucket"},
+ {"PutBucketAcl", "WriteAcp"},
+ {"GetBucketAcl", "ReadAcp"},
+ {"InvalidAction", ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ result := iamMapToStatementAction(tt.input)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+// TestIamMapToIdentitiesAction tests reverse action mapping
+func TestIamMapToIdentitiesAction(t *testing.T) {
+ // iamMapToIdentitiesAction maps internal action names to IAM statement action patterns
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {"Admin", "*"},
+ {"Read", "Get*"},
+ {"Write", "Put*"},
+ {"List", "List*"},
+ {"Tagging", "Tagging*"},
+ {"Unknown", ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ result := iamMapToIdentitiesAction(tt.input)
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+// TestEmbeddedIamGetUserNotFound tests GetUser with non-existent user
+func TestEmbeddedIamGetUserNotFound(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "ExistingUser"},
+ },
+ }
+
+ userName := aws.String("NonExistentUser")
+ params := &iam.GetUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).GetUserRequest(params)
+ _ = req.Build()
+ response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.Equal(t, http.StatusNotFound, response.Code)
+}
+
+// TestEmbeddedIamDeleteUserNotFound tests DeleteUser with non-existent user
+func TestEmbeddedIamDeleteUserNotFound(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ userName := aws.String("NonExistentUser")
+ params := &iam.DeleteUserInput{UserName: userName}
+ req, _ := iam.New(session.New()).DeleteUserRequest(params)
+ _ = req.Build()
+ response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.Equal(t, http.StatusNotFound, response.Code)
+}
+
+// TestEmbeddedIamUpdateUserNotFound tests UpdateUser with non-existent user
+func TestEmbeddedIamUpdateUserNotFound(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ params := &iam.UpdateUserInput{
+ UserName: aws.String("NonExistentUser"),
+ NewUserName: aws.String("NewName"),
+ }
+ req, _ := iam.New(session.New()).UpdateUserRequest(params)
+ _ = req.Build()
+ response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.Equal(t, http.StatusBadRequest, response.Code)
+}
+
+// TestEmbeddedIamCreateAccessKeyForExistingUser tests CreateAccessKey creates credentials for existing user
+func TestEmbeddedIamCreateAccessKeyForExistingUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {Name: "ExistingUser"},
+ },
+ }
+
+ // Use direct form post
+ form := url.Values{}
+ form.Set("Action", "CreateAccessKey")
+ form.Set("UserName", "ExistingUser")
+
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.PostForm = form
+ req.Form = form
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+
+ assert.Equal(t, http.StatusOK, rr.Code)
+ // Verify credentials were created
+ assert.Len(t, api.mockConfig.Identities[0].Credentials, 1)
+}
+
+// TestEmbeddedIamGetUserPolicyUserNotFound tests GetUserPolicy with non-existent user
+func TestEmbeddedIamGetUserPolicyUserNotFound(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{}
+
+ params := &iam.GetUserPolicyInput{
+ UserName: aws.String("NonExistentUser"),
+ PolicyName: aws.String("TestPolicy"),
+ }
+ req, _ := iam.New(session.New()).GetUserPolicyRequest(params)
+ _ = req.Build()
+ response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.Equal(t, http.StatusNotFound, response.Code)
+}
+
+// TestEmbeddedIamCreatePolicyMalformed tests CreatePolicy with invalid policy document
+func TestEmbeddedIamCreatePolicyMalformed(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+
+ params := &iam.CreatePolicyInput{
+ PolicyName: aws.String("TestPolicy"),
+ PolicyDocument: aws.String("invalid json"),
+ }
+ req, _ := iam.New(session.New()).CreatePolicyRequest(params)
+ _ = req.Build()
+ response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
+ assert.Equal(t, http.StatusBadRequest, response.Code)
+}
+
+// TestEmbeddedIamListAccessKeysForUser tests listing access keys for a specific user
+func TestEmbeddedIamListAccessKeysForUser(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+ api.mockConfig = &iam_pb.S3ApiConfiguration{
+ Identities: []*iam_pb.Identity{
+ {
+ Name: "TestUser",
+ Credentials: []*iam_pb.Credential{
+ {AccessKey: "AKIATEST1", SecretKey: "secret1"},
+ {AccessKey: "AKIATEST2", SecretKey: "secret2"},
+ },
+ },
+ },
+ }
+
+ params := &iam.ListAccessKeysInput{UserName: aws.String("TestUser")}
+ req, _ := iam.New(session.New()).ListAccessKeysRequest(params)
+ _ = req.Build()
+ out := iamListAccessKeysResponse{}
+ response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, response.Code)
+
+ // Verify both access keys are listed
+ assert.Len(t, out.ListAccessKeysResult.AccessKeyMetadata, 2)
+}
+
+// TestEmbeddedIamNotImplementedAction tests handling of unimplemented actions
+func TestEmbeddedIamNotImplementedAction(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+
+ form := url.Values{}
+ form.Set("Action", "SomeUnknownAction")
+
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.PostForm = form
+ req.Form = form
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ rr := httptest.NewRecorder()
+ apiRouter := mux.NewRouter().SkipClean(true)
+ apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
+ apiRouter.ServeHTTP(rr, req)
+
+ assert.Equal(t, http.StatusNotImplemented, rr.Code)
+}
+
+// TestGetPolicyDocument tests parsing of policy documents
+func TestGetPolicyDocument(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+
+ validPolicy := `{
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Action": ["s3:GetObject"],
+ "Resource": ["arn:aws:s3:::bucket/*"]
+ }]
+ }`
+
+ doc, err := api.GetPolicyDocument(&validPolicy)
+ assert.NoError(t, err)
+ assert.Equal(t, "2012-10-17", doc.Version)
+ assert.Len(t, doc.Statement, 1)
+
+ // Test invalid JSON
+ invalidPolicy := "not valid json"
+ _, err = api.GetPolicyDocument(&invalidPolicy)
+ assert.Error(t, err)
+}
+
+// TestEmbeddedIamGetActionsFromPolicy tests action extraction from policy documents
+func TestEmbeddedIamGetActionsFromPolicy(t *testing.T) {
+ api := NewEmbeddedIamApiForTest()
+
+ // Actions must use wildcards (Get*, Put*, List*, etc.) as expected by the mapper
+ policyDoc := `{
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Action": ["s3:Get*", "s3:Put*"],
+ "Resource": ["arn:aws:s3:::mybucket/*"]
+ }]
+ }`
+
+ policy, err := api.GetPolicyDocument(&policyDoc)
+ assert.NoError(t, err)
+
+ actions, err := api.getActions(&policy)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, actions)
+ // Should have Read and Write actions for the bucket
+ // arn:aws:s3:::mybucket/* means all objects in mybucket, represented as "Action:mybucket"
+ assert.Contains(t, actions, "Read:mybucket")
+ assert.Contains(t, actions, "Write:mybucket")
+}
+