diff options
Diffstat (limited to 'weed/s3api/s3api_embedded_iam_test.go')
| -rw-r--r-- | weed/s3api/s3api_embedded_iam_test.go | 1028 |
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") +} + |
