mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-20 06:07:05 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user