feat(s3): add TagUser, UntagUser, ListUserTags IAM actions (#9572)

* feat(s3): add TagUser, UntagUser, ListUserTags IAM actions

Adds AWS IAM-compatible user tag operations on the embedded IAM
endpoint. Tags persist in the Identity proto as a repeated UserTag
field; the existing 50-tag / 128-byte-key / 256-byte-value AWS limits
are enforced. Pagination is stubbed (IsTruncated=false) since the
50-tag cap means all tags fit in a single response.

* review: validate UntagUser TagKeys entries

parseTagKeysParams now rejects empty keys and keys past
MaxUserTagKeyLength; UntagUser additionally requires at least one
TagKeys.member.N entry to match AWS validation behavior.

* review: pre-allocate user-tag merge and filter slices

mergeUserTags now allocates the combined existing+incoming capacity
up front; UntagUser builds the filtered slice via make with the full
ident.Tags capacity instead of ident.Tags[:0:0], which forced a
reallocation on every append.

* review: cover duplicate-in-request and invalid TagKeys cases

Regression tests assert TagUser rejects two members with the same key
in one request, and UntagUser rejects missing/empty/oversized TagKeys
entries.
This commit is contained in:
Chris Lu
2026-05-19 17:35:44 -07:00
committed by GitHub
parent 37b6a14b0d
commit cc5ef1b741
5 changed files with 847 additions and 194 deletions
+23
View File
@@ -494,3 +494,26 @@ type ListGroupsForUserResponse struct {
} `xml:"ListGroupsForUserResult"`
CommonResponse
}
// TagUserResponse is the response for TagUser action.
type TagUserResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ TagUserResponse"`
CommonResponse
}
// UntagUserResponse is the response for UntagUser action.
type UntagUserResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ UntagUserResponse"`
CommonResponse
}
// ListUserTagsResponse is the response for ListUserTags action.
type ListUserTagsResponse struct {
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUserTagsResponse"`
ListUserTagsResult struct {
Tags []*IAMTag `xml:"Tags>member"`
IsTruncated bool `xml:"IsTruncated"`
Marker string `xml:"Marker,omitempty"`
} `xml:"ListUserTagsResult"`
CommonResponse
}
+6
View File
@@ -187,6 +187,12 @@ message Identity {
repeated string service_account_ids = 6; // IDs of service accounts owned by this user
repeated string policy_names = 7;
bool is_static = 8; // Loaded from static config file (read-only, not editable via API)
repeated UserTag tags = 9;
}
message UserTag {
string key = 1;
string value = 2;
}
message Credential {
+259 -193
View File
File diff suppressed because it is too large Load Diff
+240 -1
View File
@@ -80,6 +80,9 @@ const (
MaxServiceAccountsPerUser = 100 // Maximum service accounts per user
MaxDescriptionLength = 1000 // Maximum description length in characters
MaxManagedPoliciesPerUser = 10 // Maximum managed policies attached to a user
MaxUserTags = 50 // Maximum tags per user
MaxUserTagKeyLength = 128 // Maximum tag key length
MaxUserTagValueLength = 256 // Maximum tag value length
)
// Type aliases for IAM response types from shared package
@@ -132,6 +135,11 @@ type (
iamDeleteGroupPolicyResponse = iamlib.DeleteGroupPolicyResponse
iamListGroupPoliciesResponse = iamlib.ListGroupPoliciesResponse
iamListGroupsForUserResponse = iamlib.ListGroupsForUserResponse
// User tag response types
iamTagUserResponse = iamlib.TagUserResponse
iamUntagUserResponse = iamlib.UntagUserResponse
iamListUserTagsResponse = iamlib.ListUserTagsResponse
iamTag = iamlib.IAMTag
)
// Helper function wrappers using shared package
@@ -206,7 +214,7 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re
s3err.WriteXMLResponse(w, r, http.StatusNotFound, errorResp)
case iam.ErrCodeEntityAlreadyExistsException:
s3err.WriteXMLResponse(w, r, http.StatusConflict, errorResp)
case iam.ErrCodeMalformedPolicyDocumentException, iam.ErrCodeInvalidInputException:
case iam.ErrCodeMalformedPolicyDocumentException, iam.ErrCodeInvalidInputException, "ValidationError":
s3err.WriteXMLResponse(w, r, http.StatusBadRequest, errorResp)
case "AccessDenied", iam.ErrCodeLimitExceededException:
s3err.WriteXMLResponse(w, r, http.StatusForbidden, errorResp)
@@ -1159,6 +1167,217 @@ func (e *EmbeddedIamApi) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, valu
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}
// parseTagListParams reads AWS query-encoded "Tags.member.N.Key" / "Tags.member.N.Value"
// pairs from form values, returning them in numeric order.
func parseTagListParams(values url.Values) ([]*iam_pb.UserTag, *iamError) {
type indexed struct {
idx int
key string
value string
}
indexes := make(map[int]*indexed)
for name, vs := range values {
if len(vs) == 0 || !strings.HasPrefix(name, "Tags.member.") {
continue
}
rest := strings.TrimPrefix(name, "Tags.member.")
dot := strings.Index(rest, ".")
if dot < 0 {
continue
}
n, err := strconv.Atoi(rest[:dot])
if err != nil || n < 1 {
continue
}
entry, ok := indexes[n]
if !ok {
entry = &indexed{idx: n}
indexes[n] = entry
}
switch rest[dot+1:] {
case "Key":
entry.key = vs[0]
case "Value":
entry.value = vs[0]
}
}
keys := make([]int, 0, len(indexes))
for k := range indexes {
keys = append(keys, k)
}
sort.Ints(keys)
tags := make([]*iam_pb.UserTag, 0, len(keys))
for _, k := range keys {
t := indexes[k]
if t.key == "" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("tag key cannot be empty")}
}
if len(t.key) > MaxUserTagKeyLength {
return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag key %q exceeds %d characters", t.key, MaxUserTagKeyLength)}
}
if len(t.value) > MaxUserTagValueLength {
return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag value for key %q exceeds %d characters", t.key, MaxUserTagValueLength)}
}
tags = append(tags, &iam_pb.UserTag{Key: t.key, Value: t.value})
}
return tags, nil
}
// parseTagKeysParams reads AWS query-encoded "TagKeys.member.N" entries and
// validates each entry is a non-empty key within MaxUserTagKeyLength.
func parseTagKeysParams(values url.Values) ([]string, *iamError) {
entries := make(map[int]string)
for name, vs := range values {
if len(vs) == 0 || !strings.HasPrefix(name, "TagKeys.member.") {
continue
}
rest := strings.TrimPrefix(name, "TagKeys.member.")
n, err := strconv.Atoi(rest)
if err != nil || n < 1 {
continue
}
entries[n] = vs[0]
}
keys := make([]int, 0, len(entries))
for k := range entries {
keys = append(keys, k)
}
sort.Ints(keys)
result := make([]string, 0, len(keys))
for _, k := range keys {
key := entries[k]
if key == "" {
return nil, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("tag key cannot be empty")}
}
if len(key) > MaxUserTagKeyLength {
return nil, &iamError{Code: "ValidationError", Error: fmt.Errorf("tag key %q exceeds %d characters", key, MaxUserTagKeyLength)}
}
result = append(result, key)
}
return result, nil
}
// mergeUserTags overwrites existing tags with matching keys and appends new ones,
// preserving original order for stable iteration.
func mergeUserTags(existing []*iam_pb.UserTag, incoming []*iam_pb.UserTag) []*iam_pb.UserTag {
index := make(map[string]int, len(existing))
merged := make([]*iam_pb.UserTag, 0, len(existing)+len(incoming))
for _, t := range existing {
merged = append(merged, &iam_pb.UserTag{Key: t.Key, Value: t.Value})
index[t.Key] = len(merged) - 1
}
for _, t := range incoming {
if i, ok := index[t.Key]; ok {
merged[i].Value = t.Value
continue
}
index[t.Key] = len(merged)
merged = append(merged, &iam_pb.UserTag{Key: t.Key, Value: t.Value})
}
return merged
}
// TagUser adds or updates tags on an IAM user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_TagUser.html
func (e *EmbeddedIamApi) TagUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamTagUserResponse, *iamError) {
resp := &iamTagUserResponse{}
userName := values.Get("UserName")
if userName == "" {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
}
incoming, iamErr := parseTagListParams(values)
if iamErr != nil {
return resp, iamErr
}
if len(incoming) == 0 {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("at least one tag is required")}
}
// Reject duplicate keys within the same request to match AWS semantics.
seen := make(map[string]bool, len(incoming))
for _, t := range incoming {
if seen[t.Key] {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("duplicate tag key %q", t.Key)}
}
seen[t.Key] = true
}
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
merged := mergeUserTags(ident.Tags, incoming)
if len(merged) > MaxUserTags {
return resp, &iamError{Code: iam.ErrCodeLimitExceededException, Error: fmt.Errorf("cannot exceed %d tags per user", MaxUserTags)}
}
ident.Tags = merged
return resp, nil
}
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}
// UntagUser removes the named tags from an IAM user. Unknown keys are silently ignored.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_UntagUser.html
func (e *EmbeddedIamApi) UntagUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamUntagUserResponse, *iamError) {
resp := &iamUntagUserResponse{}
userName := values.Get("UserName")
if userName == "" {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
}
keys, iamErr := parseTagKeysParams(values)
if iamErr != nil {
return resp, iamErr
}
if len(keys) == 0 {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("at least one TagKeys entry is required")}
}
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
if len(ident.Tags) == 0 {
return resp, nil
}
toRemove := make(map[string]bool, len(keys))
for _, k := range keys {
toRemove[k] = true
}
filtered := make([]*iam_pb.UserTag, 0, len(ident.Tags))
for _, t := range ident.Tags {
if toRemove[t.Key] {
continue
}
filtered = append(filtered, t)
}
ident.Tags = filtered
return resp, nil
}
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}
// ListUserTags returns the tags attached to an IAM user.
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserTags.html
// Pagination is not implemented: tag counts are bounded by MaxUserTags so the
// full set always fits in a single response.
func (e *EmbeddedIamApi) ListUserTags(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListUserTagsResponse, *iamError) {
resp := &iamListUserTagsResponse{}
userName := values.Get("UserName")
if userName == "" {
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
}
for _, ident := range s3cfg.Identities {
if ident.Name != userName {
continue
}
tags := make([]*iamTag, 0, len(ident.Tags))
for _, t := range ident.Tags {
tags = append(tags, &iamTag{Key: t.Key, Value: t.Value})
}
resp.ListUserTagsResult.Tags = tags
resp.ListUserTagsResult.IsTruncated = false
return resp, nil
}
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
}
// AttachUserPolicy attaches a managed policy to a user.
func (e *EmbeddedIamApi) AttachUserPolicy(ctx context.Context, values url.Values) (*iamAttachUserPolicyResponse, *iamError) {
resp := &iamAttachUserPolicyResponse{}
@@ -2267,6 +2486,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
switch action {
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListUserPolicies", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "ListServiceAccounts", "GetServiceAccount",
"GetGroup", "ListGroups", "ListAttachedGroupPolicies", "GetGroupPolicy", "ListGroupPolicies", "ListGroupsForUser",
"ListUserTags",
actionListOpenIDConnectProviders, actionGetOpenIDConnectProvider:
// Allowed read-only actions
default:
@@ -2392,6 +2612,25 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
return nil, iamErr
}
changed = false
case "TagUser":
var iamErr *iamError
response, iamErr = e.TagUser(s3cfg, values)
if iamErr != nil {
return nil, iamErr
}
case "UntagUser":
var iamErr *iamError
response, iamErr = e.UntagUser(s3cfg, values)
if iamErr != nil {
return nil, iamErr
}
case "ListUserTags":
var iamErr *iamError
response, iamErr = e.ListUserTags(s3cfg, values)
if iamErr != nil {
return nil, iamErr
}
changed = false
case "AttachUserPolicy":
var iamErr *iamError
response, iamErr = e.AttachUserPolicy(ctx, values)
@@ -0,0 +1,319 @@
package s3api
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// postTagAction issues a form-encoded IAM request via the test router.
func postTagAction(t *testing.T, api *EmbeddedIamApiForTest, form url.Values) *httptest.ResponseRecorder {
t.Helper()
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode()))
require.NoError(t, err)
req.PostForm = form
req.Form = form
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
r := mux.NewRouter().SkipClean(true)
r.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
r.ServeHTTP(rr, req)
return rr
}
func findIdentity(cfg *iam_pb.S3ApiConfiguration, name string) *iam_pb.Identity {
for _, ident := range cfg.Identities {
if ident.Name == name {
return ident
}
}
return nil
}
func TestEmbeddedIamTagUser(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{{Name: "alice"}},
}
params := &iam.TagUserInput{
UserName: aws.String("alice"),
Tags: []*iam.Tag{
{Key: aws.String("env"), Value: aws.String("prod")},
{Key: aws.String("team"), Value: aws.String("storage")},
},
}
req, _ := iam.New(session.New()).TagUserRequest(params)
require.NoError(t, req.Build())
out := iamTagUserResponse{}
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rr.Code)
ident := findIdentity(api.mockConfig, "alice")
require.NotNil(t, ident)
require.Len(t, ident.Tags, 2)
assert.Equal(t, "env", ident.Tags[0].Key)
assert.Equal(t, "prod", ident.Tags[0].Value)
assert.Equal(t, "team", ident.Tags[1].Key)
assert.Equal(t, "storage", ident.Tags[1].Value)
}
func TestEmbeddedIamListUserTags(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{
{
Name: "bob",
Tags: []*iam_pb.UserTag{
{Key: "env", Value: "stage"},
{Key: "owner", Value: "bob"},
},
},
},
}
params := &iam.ListUserTagsInput{UserName: aws.String("bob")}
req, _ := iam.New(session.New()).ListUserTagsRequest(params)
require.NoError(t, req.Build())
out := iamListUserTagsResponse{}
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rr.Code)
require.Len(t, out.ListUserTagsResult.Tags, 2)
assert.Equal(t, "env", out.ListUserTagsResult.Tags[0].Key)
assert.Equal(t, "stage", out.ListUserTagsResult.Tags[0].Value)
assert.False(t, out.ListUserTagsResult.IsTruncated)
}
func TestEmbeddedIamUntagUser(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{
{
Name: "carol",
Tags: []*iam_pb.UserTag{
{Key: "env", Value: "prod"},
{Key: "team", Value: "storage"},
{Key: "owner", Value: "carol"},
},
},
},
}
params := &iam.UntagUserInput{
UserName: aws.String("carol"),
TagKeys: []*string{aws.String("team"), aws.String("missing")},
}
req, _ := iam.New(session.New()).UntagUserRequest(params)
require.NoError(t, req.Build())
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rr.Code)
ident := findIdentity(api.mockConfig, "carol")
require.NotNil(t, ident)
require.Len(t, ident.Tags, 2)
keys := []string{ident.Tags[0].Key, ident.Tags[1].Key}
assert.ElementsMatch(t, []string{"env", "owner"}, keys)
}
func TestEmbeddedIamTagUserInvalidKeyLength(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{{Name: "dave"}},
}
longKey := strings.Repeat("k", MaxUserTagKeyLength+1)
params := &iam.TagUserInput{
UserName: aws.String("dave"),
Tags: []*iam.Tag{{Key: aws.String(longKey), Value: aws.String("v")}},
}
req, _ := iam.New(session.New()).TagUserRequest(params)
require.NoError(t, req.Build())
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.NotEqual(t, http.StatusOK, rr.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(rr)
assert.Equal(t, "ValidationError", code)
}
func TestEmbeddedIamTagUserReplacesDuplicate(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{
{
Name: "eve",
Tags: []*iam_pb.UserTag{{Key: "env", Value: "stage"}},
},
},
}
params := &iam.TagUserInput{
UserName: aws.String("eve"),
Tags: []*iam.Tag{{Key: aws.String("env"), Value: aws.String("prod")}},
}
req, _ := iam.New(session.New()).TagUserRequest(params)
require.NoError(t, req.Build())
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rr.Code)
ident := findIdentity(api.mockConfig, "eve")
require.NotNil(t, ident)
require.Len(t, ident.Tags, 1)
assert.Equal(t, "env", ident.Tags[0].Key)
assert.Equal(t, "prod", ident.Tags[0].Value)
}
func TestEmbeddedIamTagUserLimitExceeded(t *testing.T) {
api := NewEmbeddedIamApiForTest()
existing := make([]*iam_pb.UserTag, 0, MaxUserTags)
for i := 0; i < MaxUserTags; i++ {
existing = append(existing, &iam_pb.UserTag{Key: fmt.Sprintf("k%02d", i), Value: "v"})
}
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{{Name: "frank", Tags: existing}},
}
form := url.Values{}
form.Set("Action", "TagUser")
form.Set("UserName", "frank")
form.Set("Tags.member.1.Key", "extra")
form.Set("Tags.member.1.Value", "v")
rr := postTagAction(t, api, form)
assert.Equal(t, http.StatusForbidden, rr.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(rr)
assert.Equal(t, iam.ErrCodeLimitExceededException, code)
ident := findIdentity(api.mockConfig, "frank")
require.NotNil(t, ident)
assert.Len(t, ident.Tags, MaxUserTags)
}
func TestEmbeddedIamUntagUserNoOpForMissingKey(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{
{
Name: "grace",
Tags: []*iam_pb.UserTag{{Key: "env", Value: "prod"}},
},
},
}
form := url.Values{}
form.Set("Action", "UntagUser")
form.Set("UserName", "grace")
form.Set("TagKeys.member.1", "missing")
rr := postTagAction(t, api, form)
assert.Equal(t, http.StatusOK, rr.Code)
ident := findIdentity(api.mockConfig, "grace")
require.NotNil(t, ident)
require.Len(t, ident.Tags, 1)
assert.Equal(t, "env", ident.Tags[0].Key)
}
func TestEmbeddedIamTagUserRejectsDuplicateKeyInRequest(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{{Name: "henry"}},
}
form := url.Values{}
form.Set("Action", "TagUser")
form.Set("UserName", "henry")
form.Set("Tags.member.1.Key", "env")
form.Set("Tags.member.1.Value", "stage")
form.Set("Tags.member.2.Key", "env")
form.Set("Tags.member.2.Value", "prod")
rr := postTagAction(t, api, form)
assert.Equal(t, http.StatusBadRequest, rr.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(rr)
assert.Equal(t, iam.ErrCodeInvalidInputException, code)
}
func TestEmbeddedIamUntagUserRejectsInvalidKeys(t *testing.T) {
cases := []struct {
name string
form func() url.Values
}{
{
name: "no TagKeys entries",
form: func() url.Values {
v := url.Values{}
v.Set("Action", "UntagUser")
v.Set("UserName", "ivy")
return v
},
},
{
name: "empty TagKeys entry",
form: func() url.Values {
v := url.Values{}
v.Set("Action", "UntagUser")
v.Set("UserName", "ivy")
v.Set("TagKeys.member.1", "")
return v
},
},
{
name: "TagKeys entry over MaxUserTagKeyLength",
form: func() url.Values {
v := url.Values{}
v.Set("Action", "UntagUser")
v.Set("UserName", "ivy")
v.Set("TagKeys.member.1", strings.Repeat("k", MaxUserTagKeyLength+1))
return v
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{
Identities: []*iam_pb.Identity{{Name: "ivy"}},
}
rr := postTagAction(t, api, tc.form())
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
}
}
func TestEmbeddedIamTagUserNotFound(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{}
params := &iam.TagUserInput{
UserName: aws.String("ghost"),
Tags: []*iam.Tag{{Key: aws.String("env"), Value: aws.String("prod")}},
}
req, _ := iam.New(session.New()).TagUserRequest(params)
require.NoError(t, req.Build())
rr, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, rr.Code)
code, _ := extractEmbeddedIamErrorCodeAndMessage(rr)
assert.Equal(t, iam.ErrCodeNoSuchEntityException, code)
}