You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1028 lines
32 KiB
1028 lines
32 KiB
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")
|
|
}
|
|
|