mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-21 17:21:34 +00:00
* test(s3api): cover IAM inline policy aws:SourceIp + group inline gap Unit tests under weed/s3api/ drive PutUserPolicy / PutGroupPolicy → reload → VerifyActionPermission with a synthetic 127.0.0.1 request and assert that the policy's IpAddress condition flips the outcome. The user-policy cases pass on master (hydrateRuntimePolicies already routes inline docs through the policy engine, so Condition blocks are honored end- to-end). The group-policy case fails: PutGroupPolicy still returns NotImplemented, so a group inline doc never lands in the engine. Integration counterparts live under test/s3/iam/ and exercise the same paths against a live SeaweedFS S3+IAM endpoint. * s3api: support group inline policies + Condition enforcement PutGroupPolicy/GetGroupPolicy/DeleteGroupPolicy/ListGroupPolicies used to return NotImplemented in embedded IAM mode, so anything attached to a group as an inline doc — including aws:SourceIp or any other Condition — was simply unreachable. Wire the four endpoints to the credential-store methods that were already in place (memory, postgres, filer_etc all implement GroupInlinePolicyStore). On every config reload, hydrateRuntimePolicies now also walks LoadGroupInlinePolicies, registers each doc in the IAM policy engine under __inline_group_policy__/<group>/<policy>, and appends that key to Group.PolicyNames so evaluateIAMPolicies picks it up through its existing group walk. PutGroupPolicy/DeleteGroupPolicy are added to the ReloadConfiguration trigger list in DoActions. Side fix: MemoryStore.LoadConfiguration now surfaces store.groups too. Without it iam.groups never repopulated on a memory-store reload, so group policy evaluation silently no-op'd whether the policy was inline or attached. The existing tests didn't notice because no test reloaded through cm after creating a group. The NotImplemented unit test is inverted to drive the new round-trip. * s3api: drop redundant refreshIAMConfiguration from Put/DeleteGroupPolicy DoActions already triggers ReloadConfiguration for both actions via the explicit reload list, so calling refreshIAMConfiguration inline runs the load twice per request. Per PR review. * s3api: scope group-policy resource names per test; tighten deny polling - Integration test resource names get a per-test suffix so retried or parallel CI jobs don't trip EntityAlreadyExists / BucketAlreadyExists. - Deny-path Eventually loops gate on AccessDenied via a typed helper rather than any non-nil error; transient setup errors no longer end the wait prematurely. - ListGroupPolicies returns ServiceFailure when the credential manager is nil, matching Put/Get/DeleteGroupPolicy. * test(s3 iam): cover both IPv4 and IPv6 loopback in allow CIDRs CI runners with happy-eyeballs resolve `localhost` to ::1 first, in which case a 127.0.0.0/8-only allow would silently never match and the deny-driven enforcement test would hang for the allow case. Add ::1/128 to every loopback-matching policy so the allow path works regardless of which loopback family the SDK lands on.
2655 lines
89 KiB
Go
2655 lines
89 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/credential"
|
|
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
|
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// EmbeddedIamApiForTest is a testable version of EmbeddedIamApi
|
|
type EmbeddedIamApiForTest struct {
|
|
*EmbeddedIamApi
|
|
mockConfig *iam_pb.S3ApiConfiguration
|
|
}
|
|
|
|
func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
|
|
store := &memory.MemoryStore{}
|
|
store.Initialize(nil, "")
|
|
cm := &credential.CredentialManager{Store: store}
|
|
e := &EmbeddedIamApiForTest{
|
|
EmbeddedIamApi: &EmbeddedIamApi{
|
|
iam: &IdentityAccessManagement{credentialManager: cm},
|
|
credentialManager: cm,
|
|
},
|
|
mockConfig: &iam_pb.S3ApiConfiguration{},
|
|
}
|
|
var syncOnce sync.Once
|
|
e.getS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
// If mockConfig was set directly in test, sync it to store first (only once)
|
|
var syncErr error
|
|
syncOnce.Do(func() {
|
|
if e.mockConfig != nil {
|
|
syncErr = cm.SaveConfiguration(context.Background(), e.mockConfig)
|
|
}
|
|
})
|
|
if syncErr != nil {
|
|
return syncErr
|
|
}
|
|
config, err := cm.LoadConfiguration(context.Background())
|
|
if err == nil {
|
|
e.mockConfig = config
|
|
proto.Reset(s3cfg)
|
|
// Manually copy identities and other fields to avoid Merge issues with slices
|
|
s3cfg.Identities = make([]*iam_pb.Identity, len(config.Identities))
|
|
for i, ident := range config.Identities {
|
|
s3cfg.Identities[i] = proto.Clone(ident).(*iam_pb.Identity)
|
|
}
|
|
s3cfg.Policies = make([]*iam_pb.Policy, len(config.Policies))
|
|
for i, p := range config.Policies {
|
|
s3cfg.Policies[i] = proto.Clone(p).(*iam_pb.Policy)
|
|
}
|
|
s3cfg.Groups = make([]*iam_pb.Group, len(config.Groups))
|
|
for i, g := range config.Groups {
|
|
s3cfg.Groups[i] = proto.Clone(g).(*iam_pb.Group)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
e.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
|
|
return cm.SaveConfiguration(context.Background(), s3cfg)
|
|
}
|
|
e.reloadConfigurationFunc = func() error {
|
|
config, err := cm.LoadConfiguration(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.mockConfig = config
|
|
// Also refresh the IAM state so lookup functions see the updated configuration
|
|
if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return e
|
|
}
|
|
|
|
// DoActions handles IAM API actions for testing
|
|
func (e *EmbeddedIamApiForTest) DoActions(w http.ResponseWriter, r *http.Request) {
|
|
// Call the real DoActions
|
|
e.EmbeddedIamApi.DoActions(w, r)
|
|
}
|
|
|
|
// 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) {
|
|
body := response.Body.Bytes()
|
|
// Try parsing with ErrorResponse root
|
|
type localError struct {
|
|
Code string `xml:"Code"`
|
|
Message string `xml:"Message"`
|
|
}
|
|
type localResponse struct {
|
|
XMLName xml.Name `xml:"ErrorResponse"`
|
|
Error localError `xml:"Error"`
|
|
}
|
|
var lr localResponse
|
|
if err := xml.Unmarshal(body, &lr); err == nil && lr.Error.Code != "" {
|
|
return lr.Error.Code, lr.Error.Message
|
|
}
|
|
|
|
// Try parsing with Error root
|
|
type simpleError struct {
|
|
XMLName xml.Name `xml:"Error"`
|
|
Code string `xml:"Code"`
|
|
Message string `xml:"Message"`
|
|
}
|
|
var se simpleError
|
|
if err := xml.Unmarshal(body, &se); err == nil && se.Code != "" {
|
|
return se.Code, se.Message
|
|
}
|
|
|
|
var er embeddedIamErrorResponseForTest
|
|
if err := xml.Unmarshal(body, &er); err == nil {
|
|
return er.Error.Code, er.Error.Message
|
|
}
|
|
|
|
return "", ""
|
|
}
|
|
|
|
func extractEmbeddedIamRequestID(response *httptest.ResponseRecorder) string {
|
|
re := regexp.MustCompile(`<RequestId>([^<]+)</RequestId>`)
|
|
matches := re.FindStringSubmatch(response.Body.String())
|
|
if len(matches) < 2 {
|
|
return ""
|
|
}
|
|
return matches[1]
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateUserDoesNotSaveAllUsers is a regression test for the bug
|
|
// where creating a new user triggered a full SaveConfiguration over all existing
|
|
// identities (causing N file writes + reload cycles in the filer_etc store).
|
|
// The fix uses credentialManager.CreateUser for a targeted single-file write.
|
|
func TestEmbeddedIamCreateUserDoesNotSaveAllUsers(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
ctx := context.Background()
|
|
|
|
// Seed both mockConfig and the credential store directly so the test does
|
|
// not rely on the getS3ApiConfigurationFunc fixture's syncOnce side effect
|
|
// to populate the store. Pre-call assertions below fail loudly if seeding
|
|
// breaks for any reason.
|
|
existing := []string{"existing-user-1", "existing-user-2", "existing-user-3"}
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{}
|
|
for _, name := range existing {
|
|
api.mockConfig.Identities = append(api.mockConfig.Identities, &iam_pb.Identity{Name: name})
|
|
}
|
|
require.NoError(t, api.credentialManager.SaveConfiguration(ctx, api.mockConfig))
|
|
for _, name := range existing {
|
|
u, err := api.credentialManager.GetUser(ctx, name)
|
|
require.NoError(t, err, "seed precondition: %s must exist", name)
|
|
require.Equal(t, name, u.Name)
|
|
}
|
|
|
|
// Track calls to PutS3ApiConfiguration (the full-config write path).
|
|
putConfigCalls := 0
|
|
api.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
putConfigCalls++
|
|
api.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
|
|
return api.credentialManager.SaveConfiguration(ctx, s3cfg)
|
|
}
|
|
|
|
params := &iam.CreateUserInput{UserName: aws.String("new-user")}
|
|
req, _ := iam.New(session.New()).CreateUserRequest(params)
|
|
_ = req.Build()
|
|
out := iamCreateUserResponse{}
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, response.Code)
|
|
|
|
// The new user must be visible.
|
|
newUser, userErr := api.credentialManager.GetUser(ctx, "new-user")
|
|
require.NoError(t, userErr)
|
|
require.Equal(t, "new-user", newUser.Name)
|
|
|
|
// The critical assertion: full SaveConfiguration must NOT have been called.
|
|
// Previously this was called once for every CreateUser, re-writing all N user
|
|
// files and triggering N reload loops.
|
|
assert.Equal(t, 0, putConfigCalls, "CreateUser must not call PutS3ApiConfiguration (full-config save)")
|
|
|
|
// Pre-existing users must be untouched.
|
|
for _, name := range existing {
|
|
u, err := api.credentialManager.GetUser(ctx, name)
|
|
require.NoError(t, err, "pre-existing user %s should still exist", name)
|
|
assert.Equal(t, name, u.Name)
|
|
}
|
|
}
|
|
|
|
// TestEmbeddedIamCreateUserSkipPersist asserts that ExecuteAction("CreateUser",
|
|
// skipPersist=true) performs no persistent write to the credential store. The
|
|
// targeted-create optimization must respect the skipPersist contract documented
|
|
// on ExecuteAction; otherwise no-persist callers would silently leak writes.
|
|
func TestEmbeddedIamCreateUserSkipPersist(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
ctx := context.Background()
|
|
|
|
api.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
|
t.Fatalf("PutS3ApiConfiguration must not be called when skipPersist=true")
|
|
return nil
|
|
}
|
|
|
|
vals := url.Values{}
|
|
vals.Set("Action", "CreateUser")
|
|
vals.Set("UserName", "no-persist-user")
|
|
|
|
resp, iamErr := api.ExecuteAction(ctx, vals, true /*skipPersist*/, "")
|
|
require.Nil(t, iamErr)
|
|
require.NotNil(t, resp)
|
|
|
|
// The credential store must not have the user — skipPersist contract.
|
|
_, err := api.credentialManager.GetUser(ctx, "no-persist-user")
|
|
require.ErrorIs(t, err, credential.ErrUserNotFound,
|
|
"skipPersist=true must not write the new user to the credential store")
|
|
}
|
|
|
|
// 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)
|
|
assert.NotEmpty(t, response.Header().Get(request_id.AmzRequestIDHeader))
|
|
assert.Equal(t, response.Header().Get(request_id.AmzRequestIDHeader), out.ResponseMetadata.RequestId)
|
|
}
|
|
|
|
// 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: UserAccessKeyPrefix + "TEST12345", 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, UserAccessKeyPrefix+"TEST12345", 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)
|
|
}
|
|
|
|
// TestEmbeddedIamListUserPolicies tests listing inline policies for a user.
|
|
func TestEmbeddedIamListUserPolicies(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "UserWithPolicy",
|
|
Actions: []string{"Read", "Write"},
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
|
|
},
|
|
},
|
|
{
|
|
Name: "UserWithoutPolicy",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "TEST67890", SecretKey: "secret"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// List policies for user with actions
|
|
form := url.Values{}
|
|
form.Set("Action", "ListUserPolicies")
|
|
form.Set("UserName", "UserWithPolicy")
|
|
|
|
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)
|
|
assert.Contains(t, rr.Body.String(), "ListUserPoliciesResponse")
|
|
assert.Contains(t, rr.Body.String(), "PolicyNames")
|
|
assert.Contains(t, rr.Body.String(), "UserWithPolicy_policy")
|
|
|
|
// List policies for user without actions
|
|
form2 := url.Values{}
|
|
form2.Set("Action", "ListUserPolicies")
|
|
form2.Set("UserName", "UserWithoutPolicy")
|
|
|
|
req2, _ := http.NewRequest("POST", "/", nil)
|
|
req2.PostForm = form2
|
|
req2.Form = form2
|
|
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
rr2 := httptest.NewRecorder()
|
|
apiRouter.ServeHTTP(rr2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, rr2.Code)
|
|
assert.Contains(t, rr2.Body.String(), "ListUserPoliciesResponse")
|
|
|
|
// List policies for nonexistent user
|
|
form3 := url.Values{}
|
|
form3.Set("Action", "ListUserPolicies")
|
|
form3.Set("UserName", "NonExistentUser")
|
|
|
|
req3, _ := http.NewRequest("POST", "/", nil)
|
|
req3.PostForm = form3
|
|
req3.Form = form3
|
|
req3.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
rr3 := httptest.NewRecorder()
|
|
apiRouter.ServeHTTP(rr3, req3)
|
|
|
|
assert.Equal(t, http.StatusNotFound, rr3.Code)
|
|
}
|
|
|
|
// TestEmbeddedIamGroupInlinePolicies_CRUD exercises the Put/Get/List/Delete
|
|
// round-trip for group inline policies and verifies that an unknown policy
|
|
// surfaces as NoSuchEntity (not the legacy NotImplemented).
|
|
func TestEmbeddedIamGroupInlinePolicies_CRUD(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Groups: []*iam_pb.Group{
|
|
{Name: "developers", Members: []string{"alice"}},
|
|
},
|
|
}
|
|
api.mockConfig = s3cfg
|
|
|
|
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::*"}]}`
|
|
|
|
_, iamErr := api.PutGroupPolicy(s3cfg, url.Values{
|
|
"GroupName": {"developers"},
|
|
"PolicyName": {"DevPolicy"},
|
|
"PolicyDocument": {policyDoc},
|
|
})
|
|
require.Nil(t, iamErr, "PutGroupPolicy must succeed; got: %+v", iamErr)
|
|
|
|
listResp, iamErr := api.ListGroupPolicies(s3cfg, url.Values{"GroupName": {"developers"}})
|
|
require.Nil(t, iamErr)
|
|
assert.Equal(t, []string{"DevPolicy"}, listResp.ListGroupPoliciesResult.PolicyNames)
|
|
|
|
getResp, iamErr := api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
|
|
require.Nil(t, iamErr)
|
|
assert.Equal(t, "developers", getResp.GetGroupPolicyResult.GroupName)
|
|
assert.Equal(t, "DevPolicy", getResp.GetGroupPolicyResult.PolicyName)
|
|
assert.Contains(t, getResp.GetGroupPolicyResult.PolicyDocument, "s3:GetObject")
|
|
|
|
_, iamErr = api.DeleteGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
|
|
require.Nil(t, iamErr)
|
|
|
|
_, iamErr = api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
|
|
require.NotNil(t, iamErr)
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
|
|
_, iamErr = api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"ghosts"}, "PolicyName": {"DevPolicy"}})
|
|
require.NotNil(t, iamErr, "GetGroupPolicy on a missing group must error")
|
|
assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
|
|
}
|
|
|
|
// TestEmbeddedIamAttachUserPolicy tests attaching a managed policy to a user.
|
|
func TestEmbeddedIamAttachUserPolicy(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.AttachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
out := iamAttachUserPolicyResponse{}
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
|
}
|
|
|
|
func TestEmbeddedIamAttachUserPolicyRefreshesIAM(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
ctx := context.Background()
|
|
cm := api.credentialManager
|
|
user := &iam_pb.Identity{
|
|
Name: "policyRefreshUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: "REFRESHACCESS", SecretKey: "REFRESHSECRET"},
|
|
},
|
|
}
|
|
require.NoError(t, cm.CreateUser(ctx, user))
|
|
policy := policy_engine.PolicyDocument{
|
|
Version: policy_engine.PolicyVersion2012_10_17,
|
|
Statement: []policy_engine.PolicyStatement{
|
|
{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::bucket/*"),
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, cm.PutPolicy(ctx, "RefreshPolicy", policy))
|
|
require.NoError(t, api.iam.LoadS3ApiConfigurationFromCredentialManager())
|
|
|
|
identity := api.iam.lookupByIdentityName("policyRefreshUser")
|
|
require.NotNil(t, identity)
|
|
assert.Empty(t, identity.PolicyNames)
|
|
|
|
values := url.Values{}
|
|
values.Set("UserName", "policyRefreshUser")
|
|
values.Set("PolicyArn", "arn:aws:iam:::policy/RefreshPolicy")
|
|
|
|
_, iamErr := api.AttachUserPolicy(ctx, values)
|
|
require.Nil(t, iamErr)
|
|
|
|
identity = api.iam.lookupByIdentityName("policyRefreshUser")
|
|
require.NotNil(t, identity)
|
|
assert.Equal(t, []string{"RefreshPolicy"}, identity.PolicyNames)
|
|
}
|
|
|
|
// TestEmbeddedIamAttachUserPolicyNoSuchPolicy tests attach failure when managed policy does not exist.
|
|
func TestEmbeddedIamAttachUserPolicyNoSuchPolicy(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
params := &iam.AttachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/DoesNotExist"),
|
|
}
|
|
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusNotFound, response.Code)
|
|
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
|
assert.Equal(t, "NoSuchEntity", code)
|
|
}
|
|
|
|
// TestEmbeddedIamDetachUserPolicy tests detaching a managed policy from a user.
|
|
func TestEmbeddedIamDetachUserPolicy(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy", "KeepPolicy"}},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
{Name: "KeepPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.DetachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
out := iamDetachUserPolicyResponse{}
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
assert.Equal(t, []string{"KeepPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
|
}
|
|
|
|
// TestEmbeddedIamDeletePolicyInUse ensures deleting a policy that is still attached returns conflict.
|
|
func TestEmbeddedIamDeletePolicyInUse(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", PolicyNames: []string{"TestPolicy"}},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "TestPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.DeletePolicyInput{
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/TestPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).DeletePolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusConflict, response.Code)
|
|
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
|
assert.Equal(t, iam.ErrCodeDeleteConflictException, code)
|
|
|
|
assert.Len(t, api.mockConfig.Policies, 1)
|
|
assert.Equal(t, "TestPolicy", api.mockConfig.Policies[0].Name)
|
|
assert.Len(t, api.mockConfig.Identities, 1)
|
|
assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)
|
|
assert.Contains(t, api.mockConfig.Identities[0].PolicyNames, "TestPolicy")
|
|
}
|
|
|
|
// TestEmbeddedIamAttachAlreadyAttachedPolicy ensures attaching a policy already
|
|
// present on the user is idempotent.
|
|
func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy"}},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.AttachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
out := iamAttachUserPolicyResponse{}
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
|
}
|
|
|
|
// TestEmbeddedIamDetachNotAttachedPolicy verifies detaching a policy that's not
|
|
// attached returns NoSuchEntity.
|
|
func TestEmbeddedIamDetachNotAttachedPolicy(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "MissingPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.DetachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/MissingPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusNotFound, response.Code)
|
|
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
|
assert.Equal(t, "NoSuchEntity", code)
|
|
}
|
|
|
|
// TestEmbeddedIamAttachPolicyLimitExceeded ensures we honor the managed policy limit.
|
|
func TestEmbeddedIamAttachPolicyLimitExceeded(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
existingPolicies := make([]string, 0, MaxManagedPoliciesPerUser)
|
|
configPolicies := make([]*iam_pb.Policy, 0, MaxManagedPoliciesPerUser+1)
|
|
for i := 0; i < MaxManagedPoliciesPerUser; i++ {
|
|
name := fmt.Sprintf("ManagedPolicy%d", i)
|
|
existingPolicies = append(existingPolicies, name)
|
|
configPolicies = append(configPolicies, &iam_pb.Policy{
|
|
Name: name,
|
|
Content: `{"Version":"2012-10-17","Statement":[]}`,
|
|
})
|
|
}
|
|
configPolicies = append(configPolicies, &iam_pb.Policy{
|
|
Name: "NewPolicy",
|
|
Content: `{"Version":"2012-10-17","Statement":[]}`,
|
|
})
|
|
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", PolicyNames: existingPolicies},
|
|
},
|
|
Policies: configPolicies,
|
|
}
|
|
|
|
params := &iam.AttachUserPolicyInput{
|
|
UserName: aws.String("TestUser"),
|
|
PolicyArn: aws.String("arn:aws:iam:::policy/NewPolicy"),
|
|
}
|
|
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
|
_ = req.Build()
|
|
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusForbidden, response.Code)
|
|
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
|
assert.Equal(t, iam.ErrCodeLimitExceededException, code)
|
|
assert.Len(t, api.mockConfig.Identities[0].PolicyNames, MaxManagedPoliciesPerUser)
|
|
}
|
|
|
|
// TestEmbeddedIamListAttachedUserPolicies tests listing managed policies attached to a user.
|
|
func TestEmbeddedIamListAttachedUserPolicies(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", PolicyNames: []string{"PolicyA", "PolicyB"}},
|
|
},
|
|
Policies: []*iam_pb.Policy{
|
|
{Name: "PolicyA", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
{Name: "PolicyB", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
|
},
|
|
}
|
|
|
|
params := &iam.ListAttachedUserPoliciesInput{
|
|
UserName: aws.String("TestUser"),
|
|
}
|
|
req, _ := iam.New(session.New()).ListAttachedUserPoliciesRequest(params)
|
|
_ = req.Build()
|
|
|
|
out := iamListAttachedUserPoliciesResponse{}
|
|
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, response.Code)
|
|
assert.False(t, out.ListAttachedUserPoliciesResult.IsTruncated)
|
|
assert.Len(t, out.ListAttachedUserPoliciesResult.AttachedPolicies, 2)
|
|
|
|
got := map[string]string{}
|
|
for _, attached := range out.ListAttachedUserPoliciesResult.AttachedPolicies {
|
|
got[aws.StringValue(attached.PolicyName)] = aws.StringValue(attached.PolicyArn)
|
|
}
|
|
assert.Equal(t, "arn:aws:iam:::policy/PolicyA", got["PolicyA"])
|
|
assert.Equal(t, "arn:aws:iam:::policy/PolicyB", got["PolicyB"])
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// TestEmbeddedIamUpdateUserPreservesInlinePolicies makes sure renaming a
|
|
// user keeps that user's stored inline policies under the new name.
|
|
// Without the explicit policy migration in UpdateUser, SaveConfiguration
|
|
// prunes the old name and CASCADE drops the policies.
|
|
func TestEmbeddedIamUpdateUserPreservesInlinePolicies(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
const (
|
|
oldName = "RenameMe"
|
|
newName = "Renamed"
|
|
policyName = "ReadBucket"
|
|
)
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: oldName}},
|
|
}
|
|
doc := policy_engine.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy_engine.PolicyStatement{{
|
|
Effect: policy_engine.PolicyEffectAllow,
|
|
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
|
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::bucket/*"),
|
|
}},
|
|
}
|
|
require.NoError(t, api.credentialManager.PutUserInlinePolicy(context.Background(), oldName, policyName, doc))
|
|
|
|
params := &iam.UpdateUserInput{NewUserName: aws.String(newName), UserName: aws.String(oldName)}
|
|
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)
|
|
|
|
got, err := api.credentialManager.GetUserInlinePolicy(context.Background(), newName, policyName)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got, "inline policy should have been migrated to the new username")
|
|
assert.Equal(t, doc.Version, got.Version)
|
|
require.Len(t, got.Statement, 1)
|
|
assert.Equal(t, policy_engine.PolicyEffectAllow, got.Statement[0].Effect)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyRejectsMissingUser verifies CreateAccessKey
|
|
// returns NoSuchEntity for an unknown user without mutating the config.
|
|
func TestEmbeddedIamCreateAccessKeyRejectsMissingUser(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "ExistingUser"}},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "GhostUser")
|
|
|
|
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.NotEqual(t, http.StatusOK, rr.Code)
|
|
// No new identity and no credential appended to the existing one.
|
|
assert.Len(t, api.mockConfig.Identities, 1)
|
|
assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyWithCallerSuppliedKeys tests creating an access key with caller-supplied credentials
|
|
func TestEmbeddedIamCreateAccessKeyWithCallerSuppliedKeys(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", "myapp")
|
|
form.Set("SecretAccessKey", "mysecret123")
|
|
|
|
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 caller-supplied keys were used, not random ones
|
|
var out iamCreateAccessKeyResponse
|
|
err := xml.Unmarshal(rr.Body.Bytes(), &out)
|
|
require.NoError(t, err, "failed to unmarshal CreateAccessKey response")
|
|
require.NotNil(t, out.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
require.NotNil(t, out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
require.NotNil(t, out.CreateAccessKeyResult.AccessKey.UserName)
|
|
assert.Equal(t, "myapp", *out.CreateAccessKeyResult.AccessKey.AccessKeyId)
|
|
assert.Equal(t, "mysecret123", *out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
|
|
assert.Equal(t, "TestUser", *out.CreateAccessKeyResult.AccessKey.UserName)
|
|
|
|
// Verify credentials were persisted with caller-supplied keys
|
|
require.Len(t, api.mockConfig.Identities, 1)
|
|
require.Len(t, api.mockConfig.Identities[0].Credentials, 1)
|
|
assert.Equal(t, "myapp", api.mockConfig.Identities[0].Credentials[0].AccessKey)
|
|
assert.Equal(t, "mysecret123", api.mockConfig.Identities[0].Credentials[0].SecretKey)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyRejectsWeakKeys tests that weak caller-supplied keys are rejected
|
|
func TestEmbeddedIamCreateAccessKeyRejectsWeakKeys(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
// AccessKeyId too short
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", "ab")
|
|
form.Set("SecretAccessKey", "validsecret123")
|
|
|
|
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.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "AccessKeyId must be 4 to 128 alphanumeric characters")
|
|
|
|
// SecretAccessKey too short
|
|
form.Set("AccessKeyId", "validkey")
|
|
form.Set("SecretAccessKey", "short")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")
|
|
// AccessKeyId with SigV4 delimiters
|
|
form.Set("AccessKeyId", "foo/bar=baz")
|
|
form.Set("SecretAccessKey", "validsecret123")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "AccessKeyId must be 4 to 128 alphanumeric characters")
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyRejectsCollision tests that duplicate access keys are rejected
|
|
func TestEmbeddedIamCreateAccessKeyRejectsCollision(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
// Use a distinctive owner name ("ownerAlpha") so the leak assertion
|
|
// cannot accidentally match a word embedded in the error body.
|
|
const ownerName = "ownerAlpha"
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: ownerName,
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: "takenkey", SecretKey: "existingsecret"},
|
|
},
|
|
},
|
|
{Name: "NewUser"},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "NewUser")
|
|
form.Set("AccessKeyId", "takenkey")
|
|
form.Set("SecretAccessKey", "newsecret123")
|
|
|
|
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.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "already in use")
|
|
assert.NotContains(t, rr.Body.String(), ownerName, "should not leak owner name")
|
|
|
|
// Verify no credentials were added to NewUser. Look up by name because the
|
|
// memory store backs LoadConfiguration with a map, so Identities order is
|
|
// not stable across a save/load round trip.
|
|
var newUser *iam_pb.Identity
|
|
for _, ident := range api.mockConfig.Identities {
|
|
if ident.Name == "NewUser" {
|
|
newUser = ident
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, newUser, "NewUser identity should still exist")
|
|
assert.Len(t, newUser.Credentials, 0)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyRejectsPartialSupply tests that supplying only
|
|
// one of AccessKeyId / SecretAccessKey is rejected.
|
|
func TestEmbeddedIamCreateAccessKeyRejectsPartialSupply(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
apiRouter := mux.NewRouter().SkipClean(true)
|
|
apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
|
|
|
|
// AccessKeyId supplied, SecretAccessKey omitted
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", "myappkey")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "AccessKeyId and SecretAccessKey must be supplied together")
|
|
assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
|
|
|
|
// SecretAccessKey supplied, AccessKeyId omitted
|
|
form = url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("SecretAccessKey", "validsecret1")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "AccessKeyId and SecretAccessKey must be supplied together")
|
|
assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
|
|
}
|
|
|
|
// TestEmbeddedIamCreateAccessKeyBoundary tests key length boundaries
|
|
func TestEmbeddedIamCreateAccessKeyBoundary(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
apiRouter := mux.NewRouter().SkipClean(true)
|
|
apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
|
|
|
|
// Exactly 4 chars — should pass
|
|
form := url.Values{}
|
|
form.Set("Action", "CreateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", "abcd")
|
|
form.Set("SecretAccessKey", "validsecret1")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
// Exactly 3 chars — should fail
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
form.Set("AccessKeyId", "abc")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "alphanumeric")
|
|
|
|
// Exactly 128 chars — should pass
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
ak128 := strings.Repeat("a", 128)
|
|
sk128 := strings.Repeat("s", 128)
|
|
form.Set("AccessKeyId", ak128)
|
|
form.Set("SecretAccessKey", sk128)
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
// 129 chars AccessKeyId — should fail
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
form.Set("AccessKeyId", strings.Repeat("a", 129))
|
|
form.Set("SecretAccessKey", sk128)
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "alphanumeric")
|
|
|
|
// 7-char SecretAccessKey — should fail
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
form.Set("AccessKeyId", "validkey")
|
|
form.Set("SecretAccessKey", "1234567")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")
|
|
|
|
// Exactly 8-char SecretAccessKey — should pass (lower boundary)
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
form.Set("AccessKeyId", "validkey")
|
|
form.Set("SecretAccessKey", "12345678")
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
// 129-char SecretAccessKey — should fail
|
|
api.mockConfig.Identities[0].Credentials = nil
|
|
form.Set("AccessKeyId", "validkey")
|
|
form.Set("SecretAccessKey", strings.Repeat("s", 129))
|
|
|
|
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.ServeHTTP(rr, req)
|
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")
|
|
}
|
|
|
|
// 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: UserAccessKeyPrefix + "TEST12345", 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", UserAccessKeyPrefix+"TEST12345")
|
|
|
|
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: UserAccessKeyPrefix + "TESTFAKEKEY000001", 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=" + UserAccessKeyPrefix + "TESTFAKEKEY000001/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 =" + UserAccessKeyPrefix + "TESTFAKEKEY000001/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=" + UserAccessKeyPrefix + "TESTUNKNOWN000000/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.StatusNotFound, 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)
|
|
}
|
|
|
|
// TestEmbeddedIamPutGetUserPolicyRoundTrip is a regression test for
|
|
// https://github.com/seaweedfs/seaweedfs/issues/9008: put-user-policy followed
|
|
// by get-user-policy must return the same policy document, with Action and Resource
|
|
// lists intact (no wildcard expansion, no duplication, no collapsing).
|
|
func TestEmbeddedIamPutGetUserPolicyRoundTrip(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{Name: "steward"}},
|
|
}
|
|
api.mockConfig = s3cfg
|
|
|
|
policyJSON := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
|
|
"Resource": ["arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"]
|
|
}]
|
|
}`
|
|
|
|
// PutUserPolicy
|
|
_, iamErr := api.PutUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
"PolicyDocument": []string{policyJSON},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
// GetUserPolicy should return the exact document
|
|
resp, iamErr := api.GetUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
var got policy_engine.PolicyDocument
|
|
assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))
|
|
|
|
assert.Equal(t, "2012-10-17", got.Version)
|
|
require.Equal(t, 1, len(got.Statement))
|
|
stmt := got.Statement[0]
|
|
assert.Equal(t, policy_engine.PolicyEffectAllow, stmt.Effect)
|
|
assert.ElementsMatch(t, []string{"s3:GetObject", "s3:PutObject", "s3:ListBucket"}, stmt.Action.Strings())
|
|
assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, stmt.Resource.Strings())
|
|
}
|
|
|
|
// TestEmbeddedIamGetUserPolicyFallback tests the lossy fallback reconstruction
|
|
// when no stored inline policy is available (pre-existing ident.Actions only).
|
|
func TestEmbeddedIamGetUserPolicyFallback(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
s3cfg := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "steward",
|
|
Actions: []string{"Read:b-le*", "Write:b-le*", "List:b-le*", "Read:b-le*/*", "Write:b-le*/*", "List:b-le*/*"},
|
|
}},
|
|
}
|
|
api.mockConfig = s3cfg
|
|
|
|
// No stored inline policy — GetUserPolicy must reconstruct from ident.Actions
|
|
resp, iamErr := api.GetUserPolicy(s3cfg, url.Values{
|
|
"UserName": []string{"steward"},
|
|
"PolicyName": []string{"steward_policy"},
|
|
})
|
|
assert.Nil(t, iamErr)
|
|
|
|
var got policy_engine.PolicyDocument
|
|
assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))
|
|
|
|
// Fallback is lossy but must not duplicate actions
|
|
require.Equal(t, 1, len(got.Statement), "fallback should merge equal-action statements")
|
|
stmt := got.Statement[0]
|
|
assert.ElementsMatch(t, []string{"s3:Get*", "s3:Put*", "s3:List*"}, stmt.Action.Strings())
|
|
// Bucket-level and object-level resources stay distinct
|
|
assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, stmt.Resource.Strings())
|
|
}
|
|
|
|
// 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: UserAccessKeyPrefix + "TEST1", SecretKey: "secret1"},
|
|
{AccessKey: UserAccessKeyPrefix + "TEST2", 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)
|
|
assert.Contains(t, rr.Body.String(), "<RequestId>")
|
|
assert.NotContains(t, rr.Body.String(), "<ResponseMetadata>")
|
|
assert.Equal(t, rr.Header().Get(request_id.AmzRequestIDHeader), extractEmbeddedIamRequestID(rr))
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// TestEmbeddedIamPutUserPolicyAllResourceWildcard reproduces issue #9209:
|
|
// an AWS-style policy using "Action":"s3:*" with a bare "Resource":"*" is
|
|
// valid AWS IAM syntax (meaning "any resource") but was rejected with
|
|
// "no valid actions found in policy document" because the resource parser
|
|
// required a full ARN.
|
|
func TestEmbeddedIamPutUserPolicyAllResourceWildcard(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
|
|
// From issue #9209: bare "*" resource and bare "s3:*" action.
|
|
policyDoc := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Sid": "AllowS3Admin",
|
|
"Effect": "Allow",
|
|
"Action": "s3:*",
|
|
"Resource": "*"
|
|
}]
|
|
}`
|
|
|
|
policy, err := api.GetPolicyDocument(&policyDoc)
|
|
require.NoError(t, err)
|
|
|
|
actions, err := api.getActions(&policy)
|
|
require.NoError(t, err, "getActions must accept bare \"*\" resource")
|
|
assert.Contains(t, actions, ACTION_ADMIN, "s3:* should map to admin action")
|
|
}
|
|
|
|
// TestEmbeddedIamSetUserStatus tests enabling/disabling a user
|
|
func TestEmbeddedIamSetUserStatus(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
|
|
t.Run("DisableUser", func(t *testing.T) {
|
|
// Reset state for test isolation
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", Disabled: false},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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 user is now disabled
|
|
assert.True(t, api.mockConfig.Identities[0].Disabled)
|
|
})
|
|
|
|
t.Run("EnableUser", func(t *testing.T) {
|
|
// Reset state for test isolation - start with disabled user
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser", Disabled: true},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("Status", "Active")
|
|
|
|
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 user is now enabled
|
|
assert.False(t, api.mockConfig.Identities[0].Disabled)
|
|
})
|
|
}
|
|
|
|
// TestEmbeddedIamSetUserStatusErrors tests error handling for SetUserStatus
|
|
func TestEmbeddedIamSetUserStatusErrors(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{Name: "TestUser"},
|
|
},
|
|
}
|
|
|
|
t.Run("UserNotFound", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("UserName", "NonExistentUser")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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)
|
|
})
|
|
|
|
t.Run("InvalidStatus", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("Status", "InvalidStatus")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("MissingUserName", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("MissingStatus", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "SetUserStatus")
|
|
form.Set("UserName", "TestUser")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
}
|
|
|
|
// TestEmbeddedIamUpdateAccessKey tests updating access key status
|
|
func TestEmbeddedIamUpdateAccessKey(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
|
|
t.Run("DeactivateAccessKey", func(t *testing.T) {
|
|
// Reset state for test isolation
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "TestUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret", Status: "Active"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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 access key is now inactive
|
|
assert.Equal(t, "Inactive", api.mockConfig.Identities[0].Credentials[0].Status)
|
|
})
|
|
|
|
t.Run("ActivateAccessKey", func(t *testing.T) {
|
|
// Reset state for test isolation - start with inactive key
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "TestUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret", Status: "Inactive"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
form.Set("Status", "Active")
|
|
|
|
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 access key is now active
|
|
assert.Equal(t, "Active", api.mockConfig.Identities[0].Credentials[0].Status)
|
|
})
|
|
}
|
|
|
|
// TestEmbeddedIamUpdateAccessKeyErrors tests error handling for UpdateAccessKey
|
|
func TestEmbeddedIamUpdateAccessKeyErrors(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "TestUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("AccessKeyNotFound", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", "NONEXISTENT123")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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)
|
|
})
|
|
|
|
t.Run("InvalidStatus", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
form.Set("Status", "InvalidStatus")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("MissingUserName", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("MissingAccessKeyId", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("UserNotFound", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "NonExistentUser")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
form.Set("Status", "Inactive")
|
|
|
|
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)
|
|
})
|
|
|
|
t.Run("MissingStatus", func(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("Action", "UpdateAccessKey")
|
|
form.Set("UserName", "TestUser")
|
|
form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
|
|
|
|
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.StatusBadRequest, rr.Code)
|
|
})
|
|
}
|
|
|
|
// TestEmbeddedIamListAccessKeysShowsStatus tests that ListAccessKeys returns the access key status
|
|
func TestEmbeddedIamListAccessKeysShowsStatus(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "TestUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "ACTIVE123", SecretKey: "secret1", Status: "Active"},
|
|
{AccessKey: UserAccessKeyPrefix + "INACTIVE1", SecretKey: "secret2", Status: "Inactive"},
|
|
{AccessKey: UserAccessKeyPrefix + "DEFAULT12", SecretKey: "secret3"}, // No status set, should default to Active
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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 all three access keys are listed with correct status
|
|
assert.Len(t, out.ListAccessKeysResult.AccessKeyMetadata, 3)
|
|
|
|
// Find each key and verify status
|
|
statusMap := make(map[string]string)
|
|
for _, meta := range out.ListAccessKeysResult.AccessKeyMetadata {
|
|
statusMap[*meta.AccessKeyId] = *meta.Status
|
|
}
|
|
|
|
assert.Equal(t, "Active", statusMap[UserAccessKeyPrefix+"ACTIVE123"])
|
|
assert.Equal(t, "Inactive", statusMap[UserAccessKeyPrefix+"INACTIVE1"])
|
|
assert.Equal(t, "Active", statusMap[UserAccessKeyPrefix+"DEFAULT12"]) // Default to Active
|
|
}
|
|
|
|
// TestDisabledUserLookupFails tests that disabled users cannot authenticate
|
|
func TestDisabledUserLookupFails(t *testing.T) {
|
|
iam := &IdentityAccessManagement{}
|
|
testConfig := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "enabledUser",
|
|
Disabled: false,
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "ENABLED123", SecretKey: "secret1"},
|
|
},
|
|
},
|
|
{
|
|
Name: "disabledUser",
|
|
Disabled: true,
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "DISABLED12", SecretKey: "secret2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
|
|
assert.NoError(t, err)
|
|
|
|
// Enabled user should be found
|
|
identity, cred, found := iam.LookupByAccessKey(UserAccessKeyPrefix + "ENABLED123")
|
|
assert.True(t, found)
|
|
assert.NotNil(t, identity)
|
|
assert.NotNil(t, cred)
|
|
assert.Equal(t, "enabledUser", identity.Name)
|
|
|
|
// Disabled user should NOT be found
|
|
identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "DISABLED12")
|
|
assert.False(t, found)
|
|
assert.Nil(t, identity)
|
|
assert.Nil(t, cred)
|
|
}
|
|
|
|
// TestInactiveAccessKeyLookupFails tests that inactive access keys cannot authenticate
|
|
func TestInactiveAccessKeyLookupFails(t *testing.T) {
|
|
iam := &IdentityAccessManagement{}
|
|
testConfig := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "testUser",
|
|
Credentials: []*iam_pb.Credential{
|
|
{AccessKey: UserAccessKeyPrefix + "ACTIVE123", SecretKey: "secret1", Status: "Active"},
|
|
{AccessKey: UserAccessKeyPrefix + "INACTIVE1", SecretKey: "secret2", Status: "Inactive"},
|
|
{AccessKey: UserAccessKeyPrefix + "DEFAULT12", SecretKey: "secret3"}, // No status = Active
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
|
|
assert.NoError(t, err)
|
|
|
|
// Active key should be found
|
|
identity, cred, found := iam.LookupByAccessKey(UserAccessKeyPrefix + "ACTIVE123")
|
|
assert.True(t, found)
|
|
assert.NotNil(t, identity)
|
|
assert.NotNil(t, cred)
|
|
|
|
// Inactive key should NOT be found
|
|
identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "INACTIVE1")
|
|
assert.False(t, found)
|
|
assert.Nil(t, identity)
|
|
assert.Nil(t, cred)
|
|
|
|
// Key with no status (default Active) should be found
|
|
identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "DEFAULT12")
|
|
assert.True(t, found)
|
|
assert.NotNil(t, identity)
|
|
assert.NotNil(t, cred)
|
|
}
|
|
|
|
// TestAuthIamAuthenticatesBeforeParseForm verifies that AuthIam authenticates the request
|
|
// BEFORE parsing the form. This is critical because ParseForm() consumes the request body,
|
|
// but IAM signature verification needs to hash the body.
|
|
// This test reproduces the bug described in GitHub issue #7802.
|
|
func TestAuthIamAuthenticatesBeforeParseForm(t *testing.T) {
|
|
// Create IAM with test credentials
|
|
iam := &IdentityAccessManagement{
|
|
hashes: make(map[string]*sync.Pool),
|
|
hashCounters: make(map[string]*int32),
|
|
}
|
|
|
|
testConfig := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "admin",
|
|
Credentials: []*iam_pb.Credential{
|
|
{
|
|
AccessKey: "admin_access_key",
|
|
SecretKey: "admin_secret_key",
|
|
Status: "Active",
|
|
},
|
|
},
|
|
Actions: []string{"Admin"},
|
|
},
|
|
},
|
|
}
|
|
err := iam.loadS3ApiConfiguration(testConfig)
|
|
assert.NoError(t, err)
|
|
|
|
embeddedApi := &EmbeddedIamApi{
|
|
iam: iam,
|
|
}
|
|
|
|
// Create a properly signed IAM request
|
|
payload := "Action=CreateUser&Version=2010-05-08&UserName=bob"
|
|
|
|
// Use current time to avoid clock skew
|
|
now := time.Now().UTC()
|
|
amzDate := now.Format(iso8601Format)
|
|
dateStamp := now.Format(yyyymmdd)
|
|
credentialScope := dateStamp + "/us-east-1/iam/aws4_request"
|
|
|
|
req, err := http.NewRequest("POST", "http://localhost:8333/", strings.NewReader(payload))
|
|
assert.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
|
req.Header.Set("Host", "localhost:8333")
|
|
req.Header.Set("X-Amz-Date", amzDate)
|
|
|
|
// Calculate the correct signature using IAM service
|
|
payloadHash := getSHA256Hash([]byte(payload))
|
|
canonicalRequest := fmt.Sprintf("POST\n/\n\ncontent-type:application/x-www-form-urlencoded; charset=utf-8\nhost:localhost:8333\nx-amz-date:%s\n\ncontent-type;host;x-amz-date\n%s", amzDate, payloadHash)
|
|
canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest))
|
|
stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", amzDate, credentialScope, canonicalRequestHash)
|
|
signingKey := getSigningKey("admin_secret_key", dateStamp, "us-east-1", "iam")
|
|
signature := getSignature(signingKey, stringToSign)
|
|
|
|
authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=admin_access_key/%s, SignedHeaders=content-type;host;x-amz-date, Signature=%s",
|
|
credentialScope, signature)
|
|
req.Header.Set("Authorization", authHeader)
|
|
|
|
// Create a test handler that just returns OK
|
|
handlerCalled := false
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handlerCalled = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Wrap with AuthIam
|
|
authHandler := embeddedApi.AuthIam(testHandler, ACTION_WRITE)
|
|
|
|
// Execute the request
|
|
rr := httptest.NewRecorder()
|
|
authHandler.ServeHTTP(rr, req)
|
|
|
|
// The handler should be called (authentication succeeded)
|
|
// Before the fix, this would fail with SignatureDoesNotMatch because
|
|
// ParseForm was called before authentication, consuming the body
|
|
assert.True(t, handlerCalled, "Handler was not called - authentication failed")
|
|
assert.Equal(t, http.StatusOK, rr.Code, "Expected OK status, got %d", rr.Code)
|
|
}
|
|
|
|
// TestOldCodeOrderWouldFail demonstrates why the old code order was broken.
|
|
// This test shows that ParseForm() before signature verification causes auth failure.
|
|
func TestOldCodeOrderWouldFail(t *testing.T) {
|
|
// Create IAM with test credentials
|
|
iam := &IdentityAccessManagement{
|
|
hashes: make(map[string]*sync.Pool),
|
|
hashCounters: make(map[string]*int32),
|
|
}
|
|
|
|
testConfig := &iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{
|
|
{
|
|
Name: "admin",
|
|
Credentials: []*iam_pb.Credential{
|
|
{
|
|
AccessKey: "admin_access_key",
|
|
SecretKey: "admin_secret_key",
|
|
Status: "Active",
|
|
},
|
|
},
|
|
Actions: []string{"Admin"},
|
|
},
|
|
},
|
|
}
|
|
err := iam.loadS3ApiConfiguration(testConfig)
|
|
assert.NoError(t, err)
|
|
|
|
// Create a properly signed IAM request
|
|
payload := "Action=CreateUser&Version=2010-05-08&UserName=bob"
|
|
|
|
now := time.Now().UTC()
|
|
amzDate := now.Format(iso8601Format)
|
|
dateStamp := now.Format(yyyymmdd)
|
|
credentialScope := dateStamp + "/us-east-1/iam/aws4_request"
|
|
|
|
req, err := http.NewRequest("POST", "http://localhost:8333/", strings.NewReader(payload))
|
|
assert.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
|
req.Header.Set("Host", "localhost:8333")
|
|
req.Header.Set("X-Amz-Date", amzDate)
|
|
|
|
// Calculate the correct signature using IAM service
|
|
payloadHash := getSHA256Hash([]byte(payload))
|
|
canonicalRequest := fmt.Sprintf("POST\n/\n\ncontent-type:application/x-www-form-urlencoded; charset=utf-8\nhost:localhost:8333\nx-amz-date:%s\n\ncontent-type;host;x-amz-date\n%s", amzDate, payloadHash)
|
|
canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest))
|
|
stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", amzDate, credentialScope, canonicalRequestHash)
|
|
signingKey := getSigningKey("admin_secret_key", dateStamp, "us-east-1", "iam")
|
|
signature := getSignature(signingKey, stringToSign)
|
|
|
|
authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=admin_access_key/%s, SignedHeaders=content-type;host;x-amz-date, Signature=%s",
|
|
credentialScope, signature)
|
|
req.Header.Set("Authorization", authHeader)
|
|
|
|
// Simulate OLD buggy code: ParseForm BEFORE authentication
|
|
// This consumes the request body!
|
|
err = req.ParseForm()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "CreateUser", req.Form.Get("Action")) // Form parsing works
|
|
|
|
// Now try to authenticate - this should FAIL because body is consumed
|
|
identity, errCode := iam.AuthSignatureOnly(req)
|
|
|
|
// With old code order, this would fail with SignatureDoesNotMatch
|
|
// because the body is empty when signature verification tries to hash it
|
|
assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode,
|
|
"Expected SignatureDoesNotMatch when ParseForm is called before auth")
|
|
assert.Nil(t, identity)
|
|
|
|
t.Log("This demonstrates the bug: ParseForm before auth causes SignatureDoesNotMatch")
|
|
}
|
|
|
|
// TestEmbeddedIamExecuteAction tests calling ExecuteAction directly
|
|
func TestEmbeddedIamExecuteAction(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.mockConfig = &iam_pb.S3ApiConfiguration{}
|
|
|
|
// Test case: CreateUser via ExecuteAction
|
|
vals := url.Values{}
|
|
vals.Set("Action", "CreateUser")
|
|
vals.Set("UserName", "ExecuteActionUser")
|
|
|
|
ctx := context.Background()
|
|
resp, iamErr := api.ExecuteAction(ctx, vals, false, "")
|
|
assert.Nil(t, iamErr)
|
|
|
|
// Verify response type
|
|
createResp, ok := resp.(*iamCreateUserResponse)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "ExecuteActionUser", *createResp.CreateUserResult.User.UserName)
|
|
|
|
// CreateUser now uses a targeted write (credentialManager.CreateUser) rather than
|
|
// SaveConfiguration, so persistence is checked via the credential manager directly.
|
|
user, err := api.credentialManager.GetUser(ctx, "ExecuteActionUser")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "ExecuteActionUser", user.Name)
|
|
}
|
|
|
|
// TestEmbeddedIamReadOnly tests that write operations are blocked when readOnly is true
|
|
func TestEmbeddedIamReadOnly(t *testing.T) {
|
|
api := NewEmbeddedIamApiForTest()
|
|
api.readOnly = true
|
|
|
|
// Try CreateUser (Write)
|
|
userName := aws.String("ReadOnlyUser")
|
|
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.StatusForbidden, response.Code)
|
|
|
|
code, msg := extractEmbeddedIamErrorCodeAndMessage(response)
|
|
assert.Equal(t, "AccessDenied", code)
|
|
assert.Contains(t, msg, "IAM write operations are disabled")
|
|
|
|
// Try ListUsers (Read) - Should succeed
|
|
paramsList := &iam.ListUsersInput{}
|
|
reqList, _ := iam.New(session.New()).ListUsersRequest(paramsList)
|
|
_ = reqList.Build()
|
|
|
|
outList := iamListUsersResponse{}
|
|
responseList, err := executeEmbeddedIamRequest(api, reqList.HTTPRequest, &outList)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, responseList.Code)
|
|
}
|