mirror of
https://github.com/versity/versitygw.git
synced 2026-09-23 08:24:17 +00:00
Merge pull request #2314 from versity/sis/iam-user-tagging
feat: add IAM user tagging actions
This commit is contained in:
@@ -479,31 +479,16 @@ func PutObjectRetention_shorten_governance_with_bypass(s *S3Conf) error {
|
||||
func PutObjectRetention_shorten_compliance_denied(s *S3Conf) error {
|
||||
testName := "PutObjectRetention_shorten_compliance_denied"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
date := time.Now().Add(complianceTestRetention)
|
||||
obj := "my-obj"
|
||||
_, err := putObjects(s3client, []string{obj}, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Retention: &types.ObjectLockRetention{
|
||||
Mode: types.ObjectLockRetentionModeCompliance,
|
||||
RetainUntilDate: &date,
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%v"`, s.awsID), `["s3:BypassGovernanceRetention"]`, fmt.Sprintf(`"arn:aws:s3:::%v/*"`, bucket))
|
||||
bypass := true
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
Bucket: &bucket,
|
||||
Policy: &policy,
|
||||
@@ -513,6 +498,27 @@ func PutObjectRetention_shorten_compliance_denied(s *S3Conf) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// The COMPLIANCE lock goes on last, after the slow setup above rather
|
||||
// than before it, so the whole complianceTestRetention budget is left
|
||||
// for the two attempts below. Started any earlier, a slow object upload
|
||||
// can consume the entire window and leave an already expired retention
|
||||
// with nothing to shorten.
|
||||
date := time.Now().Add(complianceTestRetention)
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Retention: &types.ObjectLockRetention{
|
||||
Mode: types.ObjectLockRetentionModeCompliance,
|
||||
RetainUntilDate: &date,
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Recomputed fresh right before each request below, rather than once
|
||||
// up front: the server rejects a RetainUntilDate that has already
|
||||
// passed (InvalidArgument) before it ever gets to the object-lock
|
||||
|
||||
@@ -1196,6 +1196,51 @@ func TestIAMUpdateUser(ts *TestState) {
|
||||
ts.Run(IAMUpdateUser_success)
|
||||
}
|
||||
|
||||
func TestIAMTagUser(ts *TestState) {
|
||||
ts.Run(IAMTagUser_missing_user_name)
|
||||
ts.Run(IAMTagUser_invalid_user_name)
|
||||
ts.Run(IAMTagUser_user_name_too_long)
|
||||
ts.Run(IAMTagUser_missing_tags)
|
||||
ts.Run(IAMTagUser_missing_tag_key)
|
||||
ts.Run(IAMTagUser_missing_tag_value)
|
||||
ts.Run(IAMTagUser_empty_tag_key)
|
||||
ts.Run(IAMTagUser_tag_key_too_long)
|
||||
ts.Run(IAMTagUser_invalid_tag_key)
|
||||
ts.Run(IAMTagUser_tag_value_too_long)
|
||||
ts.Run(IAMTagUser_invalid_tag_value)
|
||||
ts.Run(IAMTagUser_duplicate_tag_keys)
|
||||
ts.Run(IAMTagUser_too_many_tags)
|
||||
ts.Run(IAMTagUser_non_existing_user)
|
||||
ts.Run(IAMTagUser_tag_limit_exceeded)
|
||||
ts.Run(IAMTagUser_success)
|
||||
ts.Run(IAMTagUser_overwrites_existing_tag)
|
||||
}
|
||||
|
||||
func TestIAMUntagUser(ts *TestState) {
|
||||
ts.Run(IAMUntagUser_missing_user_name)
|
||||
ts.Run(IAMUntagUser_invalid_user_name)
|
||||
ts.Run(IAMUntagUser_user_name_too_long)
|
||||
ts.Run(IAMUntagUser_missing_tag_keys)
|
||||
ts.Run(IAMUntagUser_invalid_tag_key)
|
||||
ts.Run(IAMUntagUser_too_many_tag_keys)
|
||||
ts.Run(IAMUntagUser_non_existing_user)
|
||||
ts.Run(IAMUntagUser_success)
|
||||
ts.Run(IAMUntagUser_removal_is_idempotent)
|
||||
ts.Run(IAMUntagUser_case_insensitive_key)
|
||||
}
|
||||
|
||||
func TestIAMListUserTags(ts *TestState) {
|
||||
ts.Run(IAMListUserTags_missing_user_name)
|
||||
ts.Run(IAMListUserTags_invalid_user_name)
|
||||
ts.Run(IAMListUserTags_user_name_too_long)
|
||||
ts.Run(IAMListUserTags_invalid_max_items)
|
||||
ts.Run(IAMListUserTags_invalid_max_items_format)
|
||||
ts.Run(IAMListUserTags_non_existing_user)
|
||||
ts.Run(IAMListUserTags_empty_result)
|
||||
ts.Run(IAMListUserTags_success)
|
||||
ts.Run(IAMListUserTags_pagination)
|
||||
}
|
||||
|
||||
func TestIAMCreateAccessKey(ts *TestState) {
|
||||
ts.Run(IAMCreateAccessKey_missing_user_name)
|
||||
ts.Run(IAMCreateAccessKey_invalid_user_name)
|
||||
@@ -1358,6 +1403,53 @@ func TestIAMUpdateAssumeRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMUpdateAssumeRolePolicy_trust_policy_document_grammar)
|
||||
}
|
||||
|
||||
func TestIAMTagRole(ts *TestState) {
|
||||
ts.Run(IAMTagRole_missing_role_name)
|
||||
ts.Run(IAMTagRole_invalid_role_name)
|
||||
ts.Run(IAMTagRole_role_name_too_long)
|
||||
ts.Run(IAMTagRole_missing_tags)
|
||||
ts.Run(IAMTagRole_missing_tag_key)
|
||||
ts.Run(IAMTagRole_missing_tag_value)
|
||||
ts.Run(IAMTagRole_empty_tag_key)
|
||||
ts.Run(IAMTagRole_tag_key_too_long)
|
||||
ts.Run(IAMTagRole_invalid_tag_key)
|
||||
ts.Run(IAMTagRole_tag_value_too_long)
|
||||
ts.Run(IAMTagRole_invalid_tag_value)
|
||||
ts.Run(IAMTagRole_duplicate_tag_keys)
|
||||
ts.Run(IAMTagRole_too_many_tags)
|
||||
ts.Run(IAMTagRole_non_existing_role)
|
||||
ts.Run(IAMTagRole_tag_limit_exceeded)
|
||||
ts.Run(IAMTagRole_success)
|
||||
ts.Run(IAMTagRole_overwrites_existing_tag)
|
||||
ts.Run(IAMTagRole_isolated_from_same_named_user)
|
||||
}
|
||||
|
||||
func TestIAMUntagRole(ts *TestState) {
|
||||
ts.Run(IAMUntagRole_missing_role_name)
|
||||
ts.Run(IAMUntagRole_invalid_role_name)
|
||||
ts.Run(IAMUntagRole_role_name_too_long)
|
||||
ts.Run(IAMUntagRole_missing_tag_keys)
|
||||
ts.Run(IAMUntagRole_invalid_tag_key)
|
||||
ts.Run(IAMUntagRole_too_many_tag_keys)
|
||||
ts.Run(IAMUntagRole_non_existing_role)
|
||||
ts.Run(IAMUntagRole_success)
|
||||
ts.Run(IAMUntagRole_removal_is_idempotent)
|
||||
ts.Run(IAMUntagRole_case_insensitive_key)
|
||||
ts.Run(IAMUntagRole_removes_only_named_keys)
|
||||
}
|
||||
|
||||
func TestIAMListRoleTags(ts *TestState) {
|
||||
ts.Run(IAMListRoleTags_missing_role_name)
|
||||
ts.Run(IAMListRoleTags_invalid_role_name)
|
||||
ts.Run(IAMListRoleTags_role_name_too_long)
|
||||
ts.Run(IAMListRoleTags_invalid_max_items)
|
||||
ts.Run(IAMListRoleTags_invalid_max_items_format)
|
||||
ts.Run(IAMListRoleTags_non_existing_role)
|
||||
ts.Run(IAMListRoleTags_empty_result)
|
||||
ts.Run(IAMListRoleTags_success)
|
||||
ts.Run(IAMListRoleTags_pagination)
|
||||
}
|
||||
|
||||
func TestIAMPutRolePolicy(ts *TestState) {
|
||||
ts.Run(IAMPutRolePolicy_missing_role_name)
|
||||
ts.Run(IAMPutRolePolicy_missing_policy_name)
|
||||
@@ -1463,6 +1555,47 @@ func TestIAMUpdateOpenIDConnectProviderThumbprint(ts *TestState) {
|
||||
ts.Run(IAMUpdateOpenIDConnectProviderThumbprint_boundary_max_thumbprints)
|
||||
}
|
||||
|
||||
func TestIAMTagOpenIDConnectProvider(ts *TestState) {
|
||||
ts.Run(IAMTagOpenIDConnectProvider_missing_arn)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_invalid_arn)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_missing_tags)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_missing_tag_key)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_missing_tag_value)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_invalid_tags)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_duplicate_tag_keys)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_too_many_tags)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_non_existing_provider)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_tag_limit_exceeded)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_success)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_overwrites_existing_tag)
|
||||
ts.Run(IAMTagOpenIDConnectProvider_isolated_per_provider)
|
||||
}
|
||||
|
||||
func TestIAMUntagOpenIDConnectProvider(ts *TestState) {
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_missing_arn)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_invalid_arn)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_missing_tag_keys)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_invalid_tag_key)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_too_many_tag_keys)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_non_existing_provider)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_success)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_removal_is_idempotent)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_case_sensitive_key)
|
||||
ts.Run(IAMUntagOpenIDConnectProvider_removes_only_named_keys)
|
||||
}
|
||||
|
||||
func TestIAMListOpenIDConnectProviderTags(ts *TestState) {
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_missing_arn)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_invalid_arn)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_invalid_max_items)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_invalid_max_items_format)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_non_existing_provider)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_empty_result)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_success)
|
||||
ts.Run(IAMListOpenIDConnectProviderTags_pagination)
|
||||
}
|
||||
|
||||
func TestIAMAssumeRoleWithWebIdentity(ts *TestState) {
|
||||
ts.Run(IAMAssumeRoleWithWebIdentity_missing_role_arn)
|
||||
ts.Run(IAMAssumeRoleWithWebIdentity_role_arn_too_short)
|
||||
@@ -1637,6 +1770,9 @@ func TestIAM(ts *TestState) {
|
||||
TestIAMListUsers(ts)
|
||||
TestIAMDeleteUser(ts)
|
||||
TestIAMUpdateUser(ts)
|
||||
TestIAMTagUser(ts)
|
||||
TestIAMUntagUser(ts)
|
||||
TestIAMListUserTags(ts)
|
||||
TestIAMCreateAccessKey(ts)
|
||||
TestIAMUpdateAccessKey(ts)
|
||||
TestIAMDeleteAccessKey(ts)
|
||||
@@ -1651,6 +1787,9 @@ func TestIAM(ts *TestState) {
|
||||
TestIAMListRoles(ts)
|
||||
TestIAMDeleteRole(ts)
|
||||
TestIAMUpdateAssumeRolePolicy(ts)
|
||||
TestIAMTagRole(ts)
|
||||
TestIAMUntagRole(ts)
|
||||
TestIAMListRoleTags(ts)
|
||||
TestIAMPutRolePolicy(ts)
|
||||
TestIAMGetRolePolicy(ts)
|
||||
TestIAMDeleteRolePolicy(ts)
|
||||
@@ -1662,6 +1801,9 @@ func TestIAM(ts *TestState) {
|
||||
TestIAMAddClientIDToOpenIDConnectProvider(ts)
|
||||
TestIAMRemoveClientIDFromOpenIDConnectProvider(ts)
|
||||
TestIAMUpdateOpenIDConnectProviderThumbprint(ts)
|
||||
TestIAMTagOpenIDConnectProvider(ts)
|
||||
TestIAMUntagOpenIDConnectProvider(ts)
|
||||
TestIAMListOpenIDConnectProviderTags(ts)
|
||||
TestIAMAssumeRoleWithWebIdentity(ts)
|
||||
TestIAMGetCallerIdentity(ts)
|
||||
TestIAMAccessControl(ts)
|
||||
@@ -2127,6 +2269,42 @@ func GetIntTests() IntTests {
|
||||
"IAMUpdateUser_long_new_path": IAMUpdateUser_long_new_path,
|
||||
"IAMUpdateUser_new_user_name_already_exists": IAMUpdateUser_new_user_name_already_exists,
|
||||
"IAMUpdateUser_success": IAMUpdateUser_success,
|
||||
"IAMTagUser_missing_user_name": IAMTagUser_missing_user_name,
|
||||
"IAMTagUser_invalid_user_name": IAMTagUser_invalid_user_name,
|
||||
"IAMTagUser_user_name_too_long": IAMTagUser_user_name_too_long,
|
||||
"IAMTagUser_missing_tags": IAMTagUser_missing_tags,
|
||||
"IAMTagUser_missing_tag_key": IAMTagUser_missing_tag_key,
|
||||
"IAMTagUser_missing_tag_value": IAMTagUser_missing_tag_value,
|
||||
"IAMTagUser_empty_tag_key": IAMTagUser_empty_tag_key,
|
||||
"IAMTagUser_tag_key_too_long": IAMTagUser_tag_key_too_long,
|
||||
"IAMTagUser_invalid_tag_key": IAMTagUser_invalid_tag_key,
|
||||
"IAMTagUser_tag_value_too_long": IAMTagUser_tag_value_too_long,
|
||||
"IAMTagUser_invalid_tag_value": IAMTagUser_invalid_tag_value,
|
||||
"IAMTagUser_duplicate_tag_keys": IAMTagUser_duplicate_tag_keys,
|
||||
"IAMTagUser_too_many_tags": IAMTagUser_too_many_tags,
|
||||
"IAMTagUser_non_existing_user": IAMTagUser_non_existing_user,
|
||||
"IAMTagUser_tag_limit_exceeded": IAMTagUser_tag_limit_exceeded,
|
||||
"IAMTagUser_success": IAMTagUser_success,
|
||||
"IAMTagUser_overwrites_existing_tag": IAMTagUser_overwrites_existing_tag,
|
||||
"IAMUntagUser_missing_user_name": IAMUntagUser_missing_user_name,
|
||||
"IAMUntagUser_invalid_user_name": IAMUntagUser_invalid_user_name,
|
||||
"IAMUntagUser_user_name_too_long": IAMUntagUser_user_name_too_long,
|
||||
"IAMUntagUser_missing_tag_keys": IAMUntagUser_missing_tag_keys,
|
||||
"IAMUntagUser_invalid_tag_key": IAMUntagUser_invalid_tag_key,
|
||||
"IAMUntagUser_too_many_tag_keys": IAMUntagUser_too_many_tag_keys,
|
||||
"IAMUntagUser_non_existing_user": IAMUntagUser_non_existing_user,
|
||||
"IAMUntagUser_success": IAMUntagUser_success,
|
||||
"IAMUntagUser_removal_is_idempotent": IAMUntagUser_removal_is_idempotent,
|
||||
"IAMUntagUser_case_insensitive_key": IAMUntagUser_case_insensitive_key,
|
||||
"IAMListUserTags_missing_user_name": IAMListUserTags_missing_user_name,
|
||||
"IAMListUserTags_invalid_user_name": IAMListUserTags_invalid_user_name,
|
||||
"IAMListUserTags_user_name_too_long": IAMListUserTags_user_name_too_long,
|
||||
"IAMListUserTags_invalid_max_items": IAMListUserTags_invalid_max_items,
|
||||
"IAMListUserTags_invalid_max_items_format": IAMListUserTags_invalid_max_items_format,
|
||||
"IAMListUserTags_non_existing_user": IAMListUserTags_non_existing_user,
|
||||
"IAMListUserTags_empty_result": IAMListUserTags_empty_result,
|
||||
"IAMListUserTags_success": IAMListUserTags_success,
|
||||
"IAMListUserTags_pagination": IAMListUserTags_pagination,
|
||||
"IAMCreateAccessKey_missing_user_name": IAMCreateAccessKey_missing_user_name,
|
||||
"IAMCreateAccessKey_invalid_user_name": IAMCreateAccessKey_invalid_user_name,
|
||||
"IAMCreateAccessKey_long_user_name": IAMCreateAccessKey_long_user_name,
|
||||
@@ -2247,6 +2425,44 @@ func GetIntTests() IntTests {
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded": IAMUpdateAssumeRolePolicy_trust_policy_size_limit_exceeded,
|
||||
"IAMUpdateAssumeRolePolicy_success": IAMUpdateAssumeRolePolicy_success,
|
||||
"IAMUpdateAssumeRolePolicy_trust_policy_document_grammar": IAMUpdateAssumeRolePolicy_trust_policy_document_grammar,
|
||||
"IAMTagRole_missing_role_name": IAMTagRole_missing_role_name,
|
||||
"IAMTagRole_invalid_role_name": IAMTagRole_invalid_role_name,
|
||||
"IAMTagRole_role_name_too_long": IAMTagRole_role_name_too_long,
|
||||
"IAMTagRole_missing_tags": IAMTagRole_missing_tags,
|
||||
"IAMTagRole_missing_tag_key": IAMTagRole_missing_tag_key,
|
||||
"IAMTagRole_missing_tag_value": IAMTagRole_missing_tag_value,
|
||||
"IAMTagRole_empty_tag_key": IAMTagRole_empty_tag_key,
|
||||
"IAMTagRole_tag_key_too_long": IAMTagRole_tag_key_too_long,
|
||||
"IAMTagRole_invalid_tag_key": IAMTagRole_invalid_tag_key,
|
||||
"IAMTagRole_tag_value_too_long": IAMTagRole_tag_value_too_long,
|
||||
"IAMTagRole_invalid_tag_value": IAMTagRole_invalid_tag_value,
|
||||
"IAMTagRole_duplicate_tag_keys": IAMTagRole_duplicate_tag_keys,
|
||||
"IAMTagRole_too_many_tags": IAMTagRole_too_many_tags,
|
||||
"IAMTagRole_non_existing_role": IAMTagRole_non_existing_role,
|
||||
"IAMTagRole_tag_limit_exceeded": IAMTagRole_tag_limit_exceeded,
|
||||
"IAMTagRole_success": IAMTagRole_success,
|
||||
"IAMTagRole_overwrites_existing_tag": IAMTagRole_overwrites_existing_tag,
|
||||
"IAMTagRole_isolated_from_same_named_user": IAMTagRole_isolated_from_same_named_user,
|
||||
"IAMUntagRole_missing_role_name": IAMUntagRole_missing_role_name,
|
||||
"IAMUntagRole_invalid_role_name": IAMUntagRole_invalid_role_name,
|
||||
"IAMUntagRole_role_name_too_long": IAMUntagRole_role_name_too_long,
|
||||
"IAMUntagRole_missing_tag_keys": IAMUntagRole_missing_tag_keys,
|
||||
"IAMUntagRole_invalid_tag_key": IAMUntagRole_invalid_tag_key,
|
||||
"IAMUntagRole_too_many_tag_keys": IAMUntagRole_too_many_tag_keys,
|
||||
"IAMUntagRole_non_existing_role": IAMUntagRole_non_existing_role,
|
||||
"IAMUntagRole_success": IAMUntagRole_success,
|
||||
"IAMUntagRole_removal_is_idempotent": IAMUntagRole_removal_is_idempotent,
|
||||
"IAMUntagRole_case_insensitive_key": IAMUntagRole_case_insensitive_key,
|
||||
"IAMUntagRole_removes_only_named_keys": IAMUntagRole_removes_only_named_keys,
|
||||
"IAMListRoleTags_missing_role_name": IAMListRoleTags_missing_role_name,
|
||||
"IAMListRoleTags_invalid_role_name": IAMListRoleTags_invalid_role_name,
|
||||
"IAMListRoleTags_role_name_too_long": IAMListRoleTags_role_name_too_long,
|
||||
"IAMListRoleTags_invalid_max_items": IAMListRoleTags_invalid_max_items,
|
||||
"IAMListRoleTags_invalid_max_items_format": IAMListRoleTags_invalid_max_items_format,
|
||||
"IAMListRoleTags_non_existing_role": IAMListRoleTags_non_existing_role,
|
||||
"IAMListRoleTags_empty_result": IAMListRoleTags_empty_result,
|
||||
"IAMListRoleTags_success": IAMListRoleTags_success,
|
||||
"IAMListRoleTags_pagination": IAMListRoleTags_pagination,
|
||||
"IAMPutRolePolicy_missing_role_name": IAMPutRolePolicy_missing_role_name,
|
||||
"IAMPutRolePolicy_missing_policy_name": IAMPutRolePolicy_missing_policy_name,
|
||||
"IAMPutRolePolicy_missing_policy_document": IAMPutRolePolicy_missing_policy_document,
|
||||
@@ -2319,6 +2535,38 @@ func GetIntTests() IntTests {
|
||||
"IAMUpdateOpenIDConnectProviderThumbprint_non_existing_provider": IAMUpdateOpenIDConnectProviderThumbprint_non_existing_provider,
|
||||
"IAMUpdateOpenIDConnectProviderThumbprint_success": IAMUpdateOpenIDConnectProviderThumbprint_success,
|
||||
"IAMUpdateOpenIDConnectProviderThumbprint_boundary_max_thumbprints": IAMUpdateOpenIDConnectProviderThumbprint_boundary_max_thumbprints,
|
||||
"IAMTagOpenIDConnectProvider_missing_arn": IAMTagOpenIDConnectProvider_missing_arn,
|
||||
"IAMTagOpenIDConnectProvider_invalid_arn": IAMTagOpenIDConnectProvider_invalid_arn,
|
||||
"IAMTagOpenIDConnectProvider_missing_tags": IAMTagOpenIDConnectProvider_missing_tags,
|
||||
"IAMTagOpenIDConnectProvider_missing_tag_key": IAMTagOpenIDConnectProvider_missing_tag_key,
|
||||
"IAMTagOpenIDConnectProvider_missing_tag_value": IAMTagOpenIDConnectProvider_missing_tag_value,
|
||||
"IAMTagOpenIDConnectProvider_invalid_tags": IAMTagOpenIDConnectProvider_invalid_tags,
|
||||
"IAMTagOpenIDConnectProvider_duplicate_tag_keys": IAMTagOpenIDConnectProvider_duplicate_tag_keys,
|
||||
"IAMTagOpenIDConnectProvider_too_many_tags": IAMTagOpenIDConnectProvider_too_many_tags,
|
||||
"IAMTagOpenIDConnectProvider_non_existing_provider": IAMTagOpenIDConnectProvider_non_existing_provider,
|
||||
"IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup": IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup,
|
||||
"IAMTagOpenIDConnectProvider_tag_limit_exceeded": IAMTagOpenIDConnectProvider_tag_limit_exceeded,
|
||||
"IAMTagOpenIDConnectProvider_success": IAMTagOpenIDConnectProvider_success,
|
||||
"IAMTagOpenIDConnectProvider_overwrites_existing_tag": IAMTagOpenIDConnectProvider_overwrites_existing_tag,
|
||||
"IAMTagOpenIDConnectProvider_isolated_per_provider": IAMTagOpenIDConnectProvider_isolated_per_provider,
|
||||
"IAMUntagOpenIDConnectProvider_missing_arn": IAMUntagOpenIDConnectProvider_missing_arn,
|
||||
"IAMUntagOpenIDConnectProvider_invalid_arn": IAMUntagOpenIDConnectProvider_invalid_arn,
|
||||
"IAMUntagOpenIDConnectProvider_missing_tag_keys": IAMUntagOpenIDConnectProvider_missing_tag_keys,
|
||||
"IAMUntagOpenIDConnectProvider_invalid_tag_key": IAMUntagOpenIDConnectProvider_invalid_tag_key,
|
||||
"IAMUntagOpenIDConnectProvider_too_many_tag_keys": IAMUntagOpenIDConnectProvider_too_many_tag_keys,
|
||||
"IAMUntagOpenIDConnectProvider_non_existing_provider": IAMUntagOpenIDConnectProvider_non_existing_provider,
|
||||
"IAMUntagOpenIDConnectProvider_success": IAMUntagOpenIDConnectProvider_success,
|
||||
"IAMUntagOpenIDConnectProvider_removal_is_idempotent": IAMUntagOpenIDConnectProvider_removal_is_idempotent,
|
||||
"IAMUntagOpenIDConnectProvider_case_sensitive_key": IAMUntagOpenIDConnectProvider_case_sensitive_key,
|
||||
"IAMUntagOpenIDConnectProvider_removes_only_named_keys": IAMUntagOpenIDConnectProvider_removes_only_named_keys,
|
||||
"IAMListOpenIDConnectProviderTags_missing_arn": IAMListOpenIDConnectProviderTags_missing_arn,
|
||||
"IAMListOpenIDConnectProviderTags_invalid_arn": IAMListOpenIDConnectProviderTags_invalid_arn,
|
||||
"IAMListOpenIDConnectProviderTags_invalid_max_items": IAMListOpenIDConnectProviderTags_invalid_max_items,
|
||||
"IAMListOpenIDConnectProviderTags_invalid_max_items_format": IAMListOpenIDConnectProviderTags_invalid_max_items_format,
|
||||
"IAMListOpenIDConnectProviderTags_non_existing_provider": IAMListOpenIDConnectProviderTags_non_existing_provider,
|
||||
"IAMListOpenIDConnectProviderTags_empty_result": IAMListOpenIDConnectProviderTags_empty_result,
|
||||
"IAMListOpenIDConnectProviderTags_success": IAMListOpenIDConnectProviderTags_success,
|
||||
"IAMListOpenIDConnectProviderTags_pagination": IAMListOpenIDConnectProviderTags_pagination,
|
||||
"IAMAssumeRoleWithWebIdentity_missing_role_arn": IAMAssumeRoleWithWebIdentity_missing_role_arn,
|
||||
"IAMAssumeRoleWithWebIdentity_role_arn_too_short": IAMAssumeRoleWithWebIdentity_role_arn_too_short,
|
||||
"IAMAssumeRoleWithWebIdentity_malformed_duration": IAMAssumeRoleWithWebIdentity_malformed_duration,
|
||||
|
||||
@@ -133,6 +133,9 @@ func IAMCreateOpenIDConnectProvider_invalid_thumbprint(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
// IAMCreateOpenIDConnectProvider_duplicate_tag_keys covers the provider
|
||||
// actions' exact tag-key comparison: only a byte-identical repeat is a
|
||||
// duplicate, so a differently-cased repeat creates two distinct tags.
|
||||
func IAMCreateOpenIDConnectProvider_duplicate_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMCreateOpenIDConnectProvider_duplicate_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
@@ -141,10 +144,27 @@ func IAMCreateOpenIDConnectProvider_duplicate_tag_keys(s *S3Conf) error {
|
||||
ThumbprintList: []string{validOIDCThumbprint},
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("key"), Value: aws.String("one")},
|
||||
{Key: aws.String("KEY"), Value: aws.String("two")},
|
||||
{Key: aws.String("key"), Value: aws.String("two")},
|
||||
},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidInput("Duplicate tag keys found. Please note that Tag keys are case insensitive."))
|
||||
if err := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrDuplicateExactTagKeys)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arn, err := createOIDCProviderReturningArnWithTags(client, []iamtypes.Tag{
|
||||
{Key: aws.String("key"), Value: aws.String("one")},
|
||||
{Key: aws.String("KEY"), Value: aws.String("two")},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("differently-cased keys: %w", err)
|
||||
}
|
||||
|
||||
checkErr := checkIAMOIDCProviderTags(client, arn, map[string]string{"key": "one", "KEY": "two"})
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
@@ -469,6 +489,20 @@ func oidcProviderArn(providerURL string) string {
|
||||
return "arn:aws:iam::000000000000:oidc-provider/" + strings.TrimPrefix(providerURL, "https://")
|
||||
}
|
||||
|
||||
// createOIDCProviderReturningArnWithTags creates a provider at a fresh
|
||||
// random URL carrying tags and returns its ARN.
|
||||
func createOIDCProviderReturningArnWithTags(client *iam.Client, tags []iamtypes.Tag) (string, error) {
|
||||
out, err := createOIDCProvider(client, &iam.CreateOpenIDConnectProviderInput{
|
||||
Url: aws.String(newIAMOIDCProviderURL()),
|
||||
ThumbprintList: []string{validOIDCThumbprint},
|
||||
Tags: tags,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return aws.ToString(out.OpenIDConnectProviderArn), nil
|
||||
}
|
||||
|
||||
func createOIDCProviderReturningArn(client *iam.Client, thumbprints []string) (string, error) {
|
||||
out, err := createOIDCProvider(client, &iam.CreateOpenIDConnectProviderInput{
|
||||
Url: aws.String(newIAMOIDCProviderURL()),
|
||||
|
||||
@@ -53,8 +53,8 @@ func IAMDeleteRole_invalid_role_name(s *S3Conf) error {
|
||||
func IAMDeleteRole_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMDeleteRole_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
err := deleteIAMRole(client, strings.Repeat("a", 129))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
err := deleteIAMRole(client, strings.Repeat("a", 65))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ func IAMGetRole_invalid_role_name(s *S3Conf) error {
|
||||
func IAMGetRole_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMGetRole_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := getIAMRole(client, strings.Repeat("a", 129))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
_, err := getIAMRole(client, strings.Repeat("a", 65))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -63,12 +63,15 @@ func IAMListAccessKeys_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListAccessKeys_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := "non-existing-" + genRandString(16)
|
||||
for _, maxItems := range []int32{-1, 0, 1001} {
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMAccessKeys(client, &iam.ListAccessKeysInput{
|
||||
UserName: &userName,
|
||||
MaxItems: aws.Int32(maxItems),
|
||||
})
|
||||
expected := iamerr.InvalidMaxItems(fmt.Sprint(maxItems))
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
@@ -94,8 +97,7 @@ func IAMListAccessKeys_invalid_max_items_format(s *S3Conf) error {
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
expected := iamerr.ValidationError("1 validation error detected: Value 'not-a-number' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000")
|
||||
return checkIAMAuthRequest(s, req, expected)
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_missing_arn(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_missing_arn"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListOpenIDConnectProviderTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("openIDConnectProviderArn"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_invalid_arn(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_invalid_arn"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tests := []struct {
|
||||
name string
|
||||
arn string
|
||||
want iamerr.Error
|
||||
}{
|
||||
{"too_short", strings.Repeat("a", 19), iamerr.ValueTooShort("openIDConnectProviderArn", 20)},
|
||||
{"too_long", strings.Repeat("a", 2049), iamerr.ValueTooLong("openIDConnectProviderArn", 2048)},
|
||||
{"wrong_resource_type", "arn:aws:iam::000000000000:role/some-role", iamerr.ValidationError("Invalid resource type in ARN")},
|
||||
{"foreign_account_id", "arn:aws:iam::123456789012:oidc-provider/example.com", iamerr.AccessDeniedOIDCProvider("000000000000", "arn:aws:iam::123456789012:oidc-provider/example.com")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{
|
||||
OpenIDConnectProviderArn: aws.String(tt.arn),
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, tt.want); checkErr != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://validprovider.example.com")
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
MaxItems: aws.Int32(maxItems),
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_invalid_max_items_format(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_invalid_max_items_format"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListOpenIDConnectProviderTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
"OpenIDConnectProviderArn": {oidcProviderArn("https://validprovider.example.com")},
|
||||
"MaxItems": {"not-a-number"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_non_existing_provider(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_non_existing_provider"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://" + genRandString(16) + ".example.com")
|
||||
_, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{OpenIDConnectProviderArn: &arn})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityOIDCProviderDelete(arn))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_empty_result(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_empty_result"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
out, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{OpenIDConnectProviderArn: &arn})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out.Tags) != 0 {
|
||||
return fmt.Errorf("expected no tags, instead got %v", iamTagMap(out.Tags))
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_success(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createOIDCProviderReturningArnWithTags(client, []iamtypes.Tag{
|
||||
{Key: aws.String("created"), Value: aws.String("at-create-time")},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{OpenIDConnectProviderArn: &arn})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected ListOpenIDConnectProviderTags response request id")
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
// Tags supplied at creation and tags added afterwards are the
|
||||
// same set: the tag action merges into whatever create stored.
|
||||
return compareIAMTags(out.Tags, map[string]string{
|
||||
"created": "at-create-time",
|
||||
"env": "prod",
|
||||
"team": "storage",
|
||||
})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListOpenIDConnectProviderTags_pagination(s *S3Conf) error {
|
||||
testName := "IAMListOpenIDConnectProviderTags_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
want := []string{"alpha", "beta", "gamma"}
|
||||
tags := map[string]string{}
|
||||
for _, key := range want {
|
||||
tags[key] = key + "-value"
|
||||
}
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(tags),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input := iam.ListOpenIDConnectProviderTagsInput{OpenIDConnectProviderArn: &arn, MaxItems: aws.Int32(1)}
|
||||
var pages []*iam.ListOpenIDConnectProviderTagsOutput
|
||||
for {
|
||||
out, err := listIAMOIDCProviderTags(client, &input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pages = append(pages, out)
|
||||
if !out.IsTruncated {
|
||||
break
|
||||
}
|
||||
input.Marker = out.Marker
|
||||
}
|
||||
|
||||
if len(pages) != len(want) {
|
||||
return fmt.Errorf("expected %d pages, instead got %d", len(want), len(pages))
|
||||
}
|
||||
var got []string
|
||||
for i, page := range pages {
|
||||
if len(page.Tags) != 1 {
|
||||
return fmt.Errorf("expected page %d to contain 1 tag, instead got %d", i+1, len(page.Tags))
|
||||
}
|
||||
if page.IsTruncated != (i < len(pages)-1) {
|
||||
return fmt.Errorf("unexpected IsTruncated value on page %d", i+1)
|
||||
}
|
||||
got = append(got, aws.ToString(page.Tags[0].Key))
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, want) {
|
||||
return fmt.Errorf("expected tag keys %v, instead got %v", want, got)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func listIAMOIDCProviderTags(client *iam.Client, input *iam.ListOpenIDConnectProviderTagsInput) (*iam.ListOpenIDConnectProviderTagsOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.ListOpenIDConnectProviderTags(ctx, input)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func IAMListRolePolicies_invalid_max_items(s *S3Conf) error {
|
||||
_, err := listIAMRolePolicies(client, &iam.ListRolePoliciesInput{RoleName: &roleName, MaxItems: aws.Int32(1001)})
|
||||
return err
|
||||
}(),
|
||||
iamerr.InvalidMaxItems("1001"),
|
||||
iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
)
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMListRoleTags_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListRoleTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{
|
||||
RoleName: aws.String("invalid role name"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_role_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_role_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 65)),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
MaxItems: aws.Int32(maxItems),
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_invalid_max_items_format(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_invalid_max_items_format"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListRoleTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {"validrolename"},
|
||||
"MaxItems": {"not-a-number"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{RoleName: &roleName})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_empty_result(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_empty_result"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
out, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out.Tags) != 0 {
|
||||
return fmt.Errorf("expected no tags, instead got %v", iamTagMap(out.Tags))
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_success(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, iamTagList(map[string]string{"created": "at-create-time"}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected ListRoleTags response request id")
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
// Tags supplied at creation and tags added afterwards are the
|
||||
// same set: TagRole merges into whatever CreateRole stored.
|
||||
return compareIAMTags(out.Tags, map[string]string{
|
||||
"created": "at-create-time",
|
||||
"env": "prod",
|
||||
"team": "storage",
|
||||
})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListRoleTags_pagination(s *S3Conf) error {
|
||||
testName := "IAMListRoleTags_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
want := []string{"alpha", "beta", "gamma"}
|
||||
tags := map[string]string{}
|
||||
for _, key := range want {
|
||||
tags[key] = key + "-value"
|
||||
}
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{RoleName: &roleName, Tags: iamTagList(tags)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input := iam.ListRoleTagsInput{RoleName: &roleName, MaxItems: aws.Int32(1)}
|
||||
var pages []*iam.ListRoleTagsOutput
|
||||
for {
|
||||
out, err := listIAMRoleTags(client, &input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pages = append(pages, out)
|
||||
if !out.IsTruncated {
|
||||
break
|
||||
}
|
||||
input.Marker = out.Marker
|
||||
}
|
||||
|
||||
if len(pages) != len(want) {
|
||||
return fmt.Errorf("expected %d pages, instead got %d", len(want), len(pages))
|
||||
}
|
||||
var got []string
|
||||
for i, page := range pages {
|
||||
if len(page.Tags) != 1 {
|
||||
return fmt.Errorf("expected page %d to contain 1 tag, instead got %d", i+1, len(page.Tags))
|
||||
}
|
||||
if page.IsTruncated != (i < len(pages)-1) {
|
||||
return fmt.Errorf("unexpected IsTruncated value on page %d", i+1)
|
||||
}
|
||||
got = append(got, aws.ToString(page.Tags[0].Key))
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, want) {
|
||||
return fmt.Errorf("expected tag keys %v, instead got %v", want, got)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func listIAMRoleTags(client *iam.Client, input *iam.ListRoleTagsInput) (*iam.ListRoleTagsOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.ListRoleTags(ctx, input)
|
||||
}
|
||||
@@ -58,9 +58,12 @@ func IAMListRoles_long_path_prefix(s *S3Conf) error {
|
||||
func IAMListRoles_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListRoles_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, maxItems := range []int32{-1, 0, 1001} {
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMRoles(client, &iam.ListRolesInput{MaxItems: aws.Int32(maxItems)})
|
||||
expected := iamerr.ValidationError(fmt.Sprintf("1 validation error detected: Value '%d' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000", maxItems))
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
@@ -85,8 +88,7 @@ func IAMListRoles_invalid_max_items_format(s *S3Conf) error {
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
expected := iamerr.ValidationError("1 validation error detected: Value 'not-a-number' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000")
|
||||
return checkIAMAuthRequest(s, req, expected)
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func IAMListUserPolicies_invalid_max_items(s *S3Conf) error {
|
||||
_, err := listIAMUserPolicies(client, &iam.ListUserPoliciesInput{UserName: &userName, MaxItems: aws.Int32(1001)})
|
||||
return err
|
||||
}(),
|
||||
iamerr.InvalidMaxItems("1001"),
|
||||
iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
)
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMListUserTags_missing_user_name(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_missing_user_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListUserTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_invalid_user_name(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_invalid_user_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := listIAMUserTags(client, &iam.ListUserTagsInput{
|
||||
UserName: aws.String("invalid user name"),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_user_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_user_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := listIAMUserTags(client, &iam.ListUserTagsInput{
|
||||
UserName: aws.String(strings.Repeat("a", 129)),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("userName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMUserTags(client, &iam.ListUserTagsInput{
|
||||
UserName: aws.String("validusername"),
|
||||
MaxItems: aws.Int32(maxItems),
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_invalid_max_items_format(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_invalid_max_items_format"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"ListUserTags"},
|
||||
"Version": {"2010-05-08"},
|
||||
"UserName": {"validusername"},
|
||||
"MaxItems": {"not-a-number"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_non_existing_user(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_non_existing_user"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := "non-existing-" + genRandString(16)
|
||||
_, err := listIAMUserTags(client, &iam.ListUserTagsInput{UserName: &userName})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityUser(userName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_empty_result(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_empty_result"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
out, err := listIAMUserTags(client, &iam.ListUserTagsInput{UserName: &userName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out.Tags) != 0 {
|
||||
return fmt.Errorf("expected no tags, instead got %v", iamTagMap(out.Tags))
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_success(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"created": "at-create-time"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := listIAMUserTags(client, &iam.ListUserTagsInput{UserName: &userName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected ListUserTags response request id")
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
// Tags supplied at creation and tags added afterwards are the
|
||||
// same set: TagUser merges into whatever CreateUser stored.
|
||||
return compareIAMTags(out.Tags, map[string]string{
|
||||
"created": "at-create-time",
|
||||
"env": "prod",
|
||||
"team": "storage",
|
||||
})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMListUserTags_pagination(s *S3Conf) error {
|
||||
testName := "IAMListUserTags_pagination"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
want := []string{"alpha", "beta", "gamma"}
|
||||
tags := map[string]string{}
|
||||
for _, key := range want {
|
||||
tags[key] = key + "-value"
|
||||
}
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{UserName: &userName, Tags: iamTagList(tags)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input := iam.ListUserTagsInput{UserName: &userName, MaxItems: aws.Int32(1)}
|
||||
var pages []*iam.ListUserTagsOutput
|
||||
for {
|
||||
out, err := listIAMUserTags(client, &input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pages = append(pages, out)
|
||||
if !out.IsTruncated {
|
||||
break
|
||||
}
|
||||
input.Marker = out.Marker
|
||||
}
|
||||
|
||||
if len(pages) != len(want) {
|
||||
return fmt.Errorf("expected %d pages, instead got %d", len(want), len(pages))
|
||||
}
|
||||
var got []string
|
||||
for i, page := range pages {
|
||||
if len(page.Tags) != 1 {
|
||||
return fmt.Errorf("expected page %d to contain 1 tag, instead got %d", i+1, len(page.Tags))
|
||||
}
|
||||
if page.IsTruncated != (i < len(pages)-1) {
|
||||
return fmt.Errorf("unexpected IsTruncated value on page %d", i+1)
|
||||
}
|
||||
got = append(got, aws.ToString(page.Tags[0].Key))
|
||||
}
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(got, want) {
|
||||
return fmt.Errorf("expected tag keys %v, instead got %v", want, got)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func listIAMUserTags(client *iam.Client, input *iam.ListUserTagsInput) (*iam.ListUserTagsOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.ListUserTags(ctx, input)
|
||||
}
|
||||
@@ -57,9 +57,12 @@ func IAMListUsers_long_path_prefix(s *S3Conf) error {
|
||||
func IAMListUsers_invalid_max_items(s *S3Conf) error {
|
||||
testName := "IAMListUsers_invalid_max_items"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, maxItems := range []int32{-1, 0, 1001} {
|
||||
for maxItems, expected := range map[int32]iamerr.Error{
|
||||
-1: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
0: iamerr.GetAPIError(iamerr.ErrMaxItemsTooLow),
|
||||
1001: iamerr.GetAPIError(iamerr.ErrMaxItemsTooHigh),
|
||||
} {
|
||||
_, err := listIAMUsers(client, &iam.ListUsersInput{MaxItems: aws.Int32(maxItems)})
|
||||
expected := iamerr.ValidationError(fmt.Sprintf("1 validation error detected: Value '%d' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000", maxItems))
|
||||
if checkErr := checkIAMApiErr(err, expected); checkErr != nil {
|
||||
return fmt.Errorf("MaxItems %d: %w", maxItems, checkErr)
|
||||
}
|
||||
@@ -84,8 +87,7 @@ func IAMListUsers_invalid_max_items_format(s *S3Conf) error {
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
expected := iamerr.ValidationError("1 validation error detected: Value 'not-a-number' at 'maxItems' failed to satisfy constraint: Member must have value between 1 and 1000")
|
||||
return checkIAMAuthRequest(s, req, expected)
|
||||
return checkIAMAuthRequest(s, req, iamerr.MalformedInput())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/storage"
|
||||
)
|
||||
|
||||
func IAMTagOpenIDConnectProvider_missing_arn(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_missing_arn"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("openIDConnectProviderArn"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_invalid_arn(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_invalid_arn"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tests := []struct {
|
||||
name string
|
||||
arn string
|
||||
want iamerr.Error
|
||||
}{
|
||||
{"too_short", strings.Repeat("a", 19), iamerr.ValueTooShort("openIDConnectProviderArn", 20)},
|
||||
{"too_long", strings.Repeat("a", 2049), iamerr.ValueTooLong("openIDConnectProviderArn", 2048)},
|
||||
{"wrong_resource_type", "arn:aws:iam::000000000000:role/some-role", iamerr.ValidationError("Invalid resource type in ARN")},
|
||||
{"foreign_account_id", "arn:aws:iam::123456789012:oidc-provider/example.com", iamerr.AccessDeniedOIDCProvider("000000000000", "arn:aws:iam::123456789012:oidc-provider/example.com")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: aws.String(tt.arn),
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, tt.want); checkErr != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_missing_tags(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_missing_tags"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"OpenIDConnectProviderArn": {oidcProviderArn("https://validprovider.example.com")},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tags"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_missing_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_missing_tag_key"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"OpenIDConnectProviderArn": {oidcProviderArn("https://validprovider.example.com")},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_missing_tag_value(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_missing_tag_value"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"OpenIDConnectProviderArn": {oidcProviderArn("https://validprovider.example.com")},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagValue(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_invalid_tags(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_invalid_tags"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://validprovider.example.com")
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []iamtypes.Tag
|
||||
want iamerr.Error
|
||||
}{
|
||||
{"empty_key", []iamtypes.Tag{{Key: aws.String(""), Value: aws.String("prod")}}, iamerr.TagKeyTooShort(1)},
|
||||
{"key_too_long", iamTagList(map[string]string{strings.Repeat("k", 129): "prod"}), iamerr.TagKeyTooLong(1)},
|
||||
{"invalid_key", iamTagList(map[string]string{"invalid*key": "prod"}), iamerr.InvalidTagKey(1)},
|
||||
{"value_too_long", iamTagList(map[string]string{"env": strings.Repeat("v", 257)}), iamerr.TagValueTooLong(1)},
|
||||
{"invalid_value", iamTagList(map[string]string{"env": "invalid*value"}), iamerr.InvalidTagValue(1)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: tt.tags,
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, tt.want); checkErr != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagOpenIDConnectProvider_duplicate_tag_keys covers the provider
|
||||
// actions' exact tag-key comparison: an identical key twice in one request
|
||||
// is rejected, where a differently-cased repeat is two separate tags.
|
||||
func IAMTagOpenIDConnectProvider_duplicate_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_duplicate_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("prod")},
|
||||
{Key: aws.String("env"), Value: aws.String("staging")},
|
||||
},
|
||||
})
|
||||
if err := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrDuplicateExactTagKeys)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("prod")},
|
||||
{Key: aws.String("ENV"), Value: aws.String("staging")},
|
||||
},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("differently-cased keys: %w", err)
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{"env": "prod", "ENV": "staging"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_too_many_tags(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_too_many_tags"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: aws.String(oidcProviderArn("https://validprovider.example.com")),
|
||||
Tags: numberedIAMTags(1, maxIAMTagMembersPerRequest+1),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTags))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_non_existing_provider(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_non_existing_provider"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://" + genRandString(16) + ".example.com")
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityOIDCProviderDelete(arn))
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup covers a
|
||||
// malformed tag naming a provider that does not exist: the request is
|
||||
// validated in full before the provider is looked up, so the tag error
|
||||
// wins over NoSuchEntity.
|
||||
func IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_invalid_tags_precede_lookup"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: aws.String(oidcProviderArn("https://" + genRandString(16) + ".example.com")),
|
||||
Tags: iamTagList(map[string]string{"invalid*key": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_tag_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_tag_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: numberedIAMTags(1, storage.MaxTagsPerResource),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"key1": "replaced"}),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("replacing a tag at the quota: %w", err)
|
||||
}
|
||||
|
||||
// A differently-cased key is a new tag, so it overflows.
|
||||
_, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"KEY1": "x"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTagLimitExceeded))
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagOpenIDConnectProvider_success(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
// An empty tag value is legal; only the key has a minimum length.
|
||||
want := map[string]string{"env": "prod", "team": "storage", "empty": ""}
|
||||
out, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(want),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected TagOpenIDConnectProvider response request id")
|
||||
}
|
||||
|
||||
if err := checkIAMOIDCProviderTags(client, arn, want); err != nil {
|
||||
return err
|
||||
}
|
||||
// GetOpenIDConnectProvider reports the same tags the tag actions maintain.
|
||||
provider, err := getIAMOIDCProvider(client, arn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return compareIAMTags(provider.Tags, want)
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagOpenIDConnectProvider_overwrites_existing_tag covers re-tagging a
|
||||
// key that is already present: the value is replaced rather than added
|
||||
// alongside, while a differently-cased key is a separate tag that leaves
|
||||
// the original in place.
|
||||
func IAMTagOpenIDConnectProvider_overwrites_existing_tag(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_overwrites_existing_tag"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "staging"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMOIDCProviderTags(client, arn, map[string]string{"env": "staging"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"ENV": "qa"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{"env": "staging", "ENV": "qa"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagOpenIDConnectProvider_isolated_per_provider covers two providers
|
||||
// carrying the same tag keys: each set is its own, in both directions.
|
||||
func IAMTagOpenIDConnectProvider_isolated_per_provider(s *S3Conf) error {
|
||||
testName := "IAMTagOpenIDConnectProvider_isolated_per_provider"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
first, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
second, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
deleteOIDCProvider(client, first)
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &first,
|
||||
Tags: iamTagList(map[string]string{"owner": "first"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &second,
|
||||
Tags: iamTagList(map[string]string{"owner": "second"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkIAMOIDCProviderTags(client, first, map[string]string{"owner": "first"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMOIDCProviderTags(client, second, map[string]string{"owner": "second"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Untagging one must not strip the other's identically-named key.
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &first,
|
||||
TagKeys: []string{"owner"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMOIDCProviderTags(client, first, map[string]string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, second, map[string]string{"owner": "second"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, first)
|
||||
if secondErr := deleteOIDCProvider(client, second); deleteErr == nil {
|
||||
deleteErr = secondErr
|
||||
}
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func tagOIDCProvider(client *iam.Client, input *iam.TagOpenIDConnectProviderInput) (*iam.TagOpenIDConnectProviderOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.TagOpenIDConnectProvider(ctx, input)
|
||||
}
|
||||
|
||||
// checkIAMOIDCProviderTags asserts ListOpenIDConnectProviderTags reports
|
||||
// exactly want for arn.
|
||||
func checkIAMOIDCProviderTags(client *iam.Client, arn string, want map[string]string) error {
|
||||
out, err := listIAMOIDCProviderTags(client, &iam.ListOpenIDConnectProviderTagsInput{OpenIDConnectProviderArn: &arn})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return compareIAMTags(out.Tags, want)
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/storage"
|
||||
)
|
||||
|
||||
func IAMTagRole_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMTagRole_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMTagRole_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("invalid role name"),
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_role_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagRole_role_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 65)),
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_missing_tags(s *S3Conf) error {
|
||||
testName := "IAMTagRole_missing_tags"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {"validrolename"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tags"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_missing_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagRole_missing_tag_key"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {"validrolename"},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_missing_tag_value(s *S3Conf) error {
|
||||
testName := "IAMTagRole_missing_tag_value"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {"validrolename"},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagValue(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_empty_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagRole_empty_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: []iamtypes.Tag{{Key: aws.String(""), Value: aws.String("prod")}},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagKeyTooShort(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_tag_key_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagRole_tag_key_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: iamTagList(map[string]string{strings.Repeat("k", 129): "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagKeyTooLong(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_invalid_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagRole_invalid_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: iamTagList(map[string]string{"invalid*key": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_tag_value_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagRole_tag_value_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: iamTagList(map[string]string{"env": strings.Repeat("v", 257)}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagValueTooLong(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_invalid_tag_value(s *S3Conf) error {
|
||||
testName := "IAMTagRole_invalid_tag_value"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: iamTagList(map[string]string{"env": "invalid*value"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidTagValue(1))
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagRole_duplicate_tag_keys covers both an exact repeat and a
|
||||
// differently-cased repeat: IAM compares tag keys case-insensitively, so
|
||||
// both are the same key twice in one request.
|
||||
func IAMTagRole_duplicate_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMTagRole_duplicate_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, second := range []string{"env", "ENV"} {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("prod")},
|
||||
{Key: aws.String(second), Value: aws.String("staging")},
|
||||
},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrDuplicateTagKeys)); checkErr != nil {
|
||||
return fmt.Errorf("duplicate key %q: %w", second, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_too_many_tags(s *S3Conf) error {
|
||||
testName := "IAMTagRole_too_many_tags"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
Tags: numberedIAMTags(1, maxIAMTagMembersPerRequest+1),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTags))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMTagRole_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_tag_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMTagRole_tag_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: numberedIAMTags(1, storage.MaxTagsPerResource),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"key1": "replaced"}),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("replacing a tag at the quota: %w", err)
|
||||
}
|
||||
|
||||
_, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"overflow": "x"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTagLimitExceeded))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagRole_success(s *S3Conf) error {
|
||||
testName := "IAMTagRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
// An empty tag value is legal; only the key has a minimum length.
|
||||
want := map[string]string{"env": "prod", "team": "storage", "empty": ""}
|
||||
out, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(want),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected TagRole response request id")
|
||||
}
|
||||
|
||||
if err := checkIAMRoleTags(client, roleName, want); err != nil {
|
||||
return err
|
||||
}
|
||||
// GetRole reports the same tags the tag actions maintain.
|
||||
role, err := getIAMRole(client, roleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return compareIAMTags(role.Role.Tags, want)
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagRole_overwrites_existing_tag covers re-tagging a key that is
|
||||
// already present: the value is replaced rather than added alongside, and a
|
||||
// differently-cased key is the same tag — the newly supplied casing wins.
|
||||
func IAMTagRole_overwrites_existing_tag(s *S3Conf) error {
|
||||
testName := "IAMTagRole_overwrites_existing_tag"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "staging"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMRoleTags(client, roleName, map[string]string{"env": "staging"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"ENV": "qa"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMRoleTags(client, roleName, map[string]string{"ENV": "qa"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagRole_isolated_from_same_named_user covers a role and a user sharing
|
||||
// a name: they are separate entities, so tagging one leaves the other's
|
||||
// tags untouched in both directions.
|
||||
func IAMTagRole_isolated_from_same_named_user(s *S3Conf) error {
|
||||
testName := "IAMTagRole_isolated_from_same_named_user"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
name := "shared-name-" + genRandString(16)
|
||||
if _, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &name,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &name}); err != nil {
|
||||
deleteIAMRole(client, name)
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &name,
|
||||
Tags: iamTagList(map[string]string{"owner": "role"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &name,
|
||||
Tags: iamTagList(map[string]string{"owner": "user"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkIAMRoleTags(client, name, map[string]string{"owner": "role"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMUserTags(client, name, map[string]string{"owner": "user"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Untagging the user must not strip the role's identically-named key.
|
||||
if _, err := untagIAMUser(client, &iam.UntagUserInput{UserName: &name, TagKeys: []string{"owner"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMUserTags(client, name, map[string]string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMRoleTags(client, name, map[string]string{"owner": "role"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, name)
|
||||
if roleErr := deleteIAMRole(client, name); deleteErr == nil {
|
||||
deleteErr = roleErr
|
||||
}
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func tagIAMRole(client *iam.Client, input *iam.TagRoleInput) (*iam.TagRoleOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.TagRole(ctx, input)
|
||||
}
|
||||
|
||||
// createTaggableIAMRole creates a role carrying tags, if any, and returns
|
||||
// its generated name.
|
||||
func createTaggableIAMRole(client *iam.Client, tags []iamtypes.Tag) (string, error) {
|
||||
roleName := newIAMRoleName()
|
||||
_, err := createIAMRole(client, &iam.CreateRoleInput{
|
||||
RoleName: &roleName,
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
Tags: tags,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return roleName, nil
|
||||
}
|
||||
|
||||
// checkIAMRoleTags asserts ListRoleTags reports exactly want for roleName.
|
||||
func checkIAMRoleTags(client *iam.Client, roleName string, want map[string]string) error {
|
||||
out, err := listIAMRoleTags(client, &iam.ListRoleTagsInput{RoleName: &roleName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return compareIAMTags(out.Tags, want)
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/storage"
|
||||
)
|
||||
|
||||
func IAMTagUser_missing_user_name(s *S3Conf) error {
|
||||
testName := "IAMTagUser_missing_user_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_invalid_user_name(s *S3Conf) error {
|
||||
testName := "IAMTagUser_invalid_user_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("invalid user name"),
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_user_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagUser_user_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String(strings.Repeat("a", 129)),
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("userName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_missing_tags(s *S3Conf) error {
|
||||
testName := "IAMTagUser_missing_tags"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"UserName": {"validusername"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tags"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_missing_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagUser_missing_tag_key"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"UserName": {"validusername"},
|
||||
"Tags.member.1.Value": {"prod"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_missing_tag_value(s *S3Conf) error {
|
||||
testName := "IAMTagUser_missing_tag_value"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"TagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"UserName": {"validusername"},
|
||||
"Tags.member.1.Key": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingTagValue(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_empty_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagUser_empty_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: []iamtypes.Tag{{Key: aws.String(""), Value: aws.String("prod")}},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagKeyTooShort(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_tag_key_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagUser_tag_key_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: iamTagList(map[string]string{strings.Repeat("k", 129): "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagKeyTooLong(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_invalid_tag_key(s *S3Conf) error {
|
||||
testName := "IAMTagUser_invalid_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: iamTagList(map[string]string{"invalid*key": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidTagKey(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_tag_value_too_long(s *S3Conf) error {
|
||||
testName := "IAMTagUser_tag_value_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: iamTagList(map[string]string{"env": strings.Repeat("v", 257)}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.TagValueTooLong(1))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_invalid_tag_value(s *S3Conf) error {
|
||||
testName := "IAMTagUser_invalid_tag_value"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: iamTagList(map[string]string{"env": "invalid*value"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidTagValue(1))
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagUser_duplicate_tag_keys covers both an exact repeat and a
|
||||
// differently-cased repeat: IAM compares tag keys case-insensitively, so
|
||||
// both are the same key twice in one request.
|
||||
func IAMTagUser_duplicate_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMTagUser_duplicate_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, second := range []string{"env", "ENV"} {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: []iamtypes.Tag{
|
||||
{Key: aws.String("env"), Value: aws.String("prod")},
|
||||
{Key: aws.String(second), Value: aws.String("staging")},
|
||||
},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrDuplicateTagKeys)); checkErr != nil {
|
||||
return fmt.Errorf("duplicate key %q: %w", second, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_too_many_tags(s *S3Conf) error {
|
||||
testName := "IAMTagUser_too_many_tags"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
Tags: numberedIAMTags(1, maxIAMTagMembersPerRequest+1),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTags))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_non_existing_user(s *S3Conf) error {
|
||||
testName := "IAMTagUser_non_existing_user"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := "non-existing-" + genRandString(16)
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityUser(userName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_tag_limit_exceeded(s *S3Conf) error {
|
||||
testName := "IAMTagUser_tag_limit_exceeded"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: numberedIAMTags(1, storage.MaxTagsPerResource),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"key1": "replaced"}),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("replacing a tag at the quota: %w", err)
|
||||
}
|
||||
|
||||
_, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"overflow": "x"}),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTagLimitExceeded))
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func IAMTagUser_success(s *S3Conf) error {
|
||||
testName := "IAMTagUser_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
// An empty tag value is legal; only the key has a minimum length.
|
||||
want := map[string]string{"env": "prod", "team": "storage", "empty": ""}
|
||||
out, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(want),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected TagUser response request id")
|
||||
}
|
||||
|
||||
if err := checkIAMUserTags(client, userName, want); err != nil {
|
||||
return err
|
||||
}
|
||||
// GetUser reports the same tags the tag actions maintain.
|
||||
user, err := getIAMUser(client, &iam.GetUserInput{UserName: &userName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return compareIAMTags(user.User.Tags, want)
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMTagUser_overwrites_existing_tag covers re-tagging a key that is
|
||||
// already present: the value is replaced rather than added alongside, and a
|
||||
// differently-cased key is the same tag — the newly supplied casing wins.
|
||||
func IAMTagUser_overwrites_existing_tag(s *S3Conf) error {
|
||||
testName := "IAMTagUser_overwrites_existing_tag"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "staging"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMUserTags(client, userName, map[string]string{"env": "staging"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"ENV": "qa"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMUserTags(client, userName, map[string]string{"ENV": "qa"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// maxIAMTagMembersPerRequest mirrors the per-request Tags/TagKeys member
|
||||
// cap the service enforces
|
||||
const maxIAMTagMembersPerRequest = 50
|
||||
|
||||
func tagIAMUser(client *iam.Client, input *iam.TagUserInput) (*iam.TagUserOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.TagUser(ctx, input)
|
||||
}
|
||||
|
||||
func iamTagList(tags map[string]string) []iamtypes.Tag {
|
||||
list := make([]iamtypes.Tag, 0, len(tags))
|
||||
for key, value := range tags {
|
||||
list = append(list, iamtypes.Tag{Key: aws.String(key), Value: aws.String(value)})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// numberedIAMTags builds count tags named key<N>/value<N> starting at
|
||||
// first, for exercising the per-request and per-user tag quotas.
|
||||
func numberedIAMTags(first, count int) []iamtypes.Tag {
|
||||
list := make([]iamtypes.Tag, 0, count)
|
||||
for i := first; i < first+count; i++ {
|
||||
list = append(list, iamtypes.Tag{
|
||||
Key: aws.String(fmt.Sprintf("key%d", i)),
|
||||
Value: aws.String(fmt.Sprintf("value%d", i)),
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// checkIAMUserTags asserts ListUserTags reports exactly want for userName.
|
||||
func checkIAMUserTags(client *iam.Client, userName string, want map[string]string) error {
|
||||
out, err := listIAMUserTags(client, &iam.ListUserTagsInput{UserName: &userName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated to be false")
|
||||
}
|
||||
return compareIAMTags(out.Tags, want)
|
||||
}
|
||||
|
||||
func compareIAMTags(got []iamtypes.Tag, want map[string]string) error {
|
||||
if len(got) != len(want) {
|
||||
return fmt.Errorf("expected %d tags, instead got %d: %v", len(want), len(got), iamTagMap(got))
|
||||
}
|
||||
for key, value := range want {
|
||||
found, ok := iamTagMap(got)[key]
|
||||
if !ok {
|
||||
return fmt.Errorf("expected tag %q to be present, instead got %v", key, iamTagMap(got))
|
||||
}
|
||||
if found != value {
|
||||
return fmt.Errorf("expected tag %q to be %q, instead got %q", key, value, found)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func iamTagMap(tags []iamtypes.Tag) map[string]string {
|
||||
out := make(map[string]string, len(tags))
|
||||
for _, tag := range tags {
|
||||
out[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_missing_arn(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_missing_arn"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"TagKeys.member.1": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("openIDConnectProviderArn"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_invalid_arn(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_invalid_arn"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tests := []struct {
|
||||
name string
|
||||
arn string
|
||||
want iamerr.Error
|
||||
}{
|
||||
{"too_short", strings.Repeat("a", 19), iamerr.ValueTooShort("openIDConnectProviderArn", 20)},
|
||||
{"too_long", strings.Repeat("a", 2049), iamerr.ValueTooLong("openIDConnectProviderArn", 2048)},
|
||||
{"wrong_resource_type", "arn:aws:iam::000000000000:role/some-role", iamerr.ValidationError("Invalid resource type in ARN")},
|
||||
{"foreign_account_id", "arn:aws:iam::123456789012:oidc-provider/example.com", iamerr.AccessDeniedOIDCProvider("000000000000", "arn:aws:iam::123456789012:oidc-provider/example.com")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: aws.String(tt.arn),
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, tt.want); checkErr != nil {
|
||||
return fmt.Errorf("%s: %w", tt.name, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_missing_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_missing_tag_keys"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagOpenIDConnectProvider"},
|
||||
"Version": {"2010-05-08"},
|
||||
"OpenIDConnectProviderArn": {oidcProviderArn("https://validprovider.example.com")},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tagKeys"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_invalid_tag_key(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_invalid_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://validprovider.example.com")
|
||||
for _, tagKey := range []string{"", strings.Repeat("k", 129), "invalid*key"} {
|
||||
_, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{tagKey},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrInvalidTagKeys)); checkErr != nil {
|
||||
return fmt.Errorf("tag key %q: %w", tagKey, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_too_many_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_too_many_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tagKeys := make([]string, 0, maxIAMTagMembersPerRequest+1)
|
||||
for i := range maxIAMTagMembersPerRequest + 1 {
|
||||
tagKeys = append(tagKeys, fmt.Sprintf("key%d", i+1))
|
||||
}
|
||||
|
||||
_, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: aws.String(oidcProviderArn("https://validprovider.example.com")),
|
||||
TagKeys: tagKeys,
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTagKeys))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_non_existing_provider(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_non_existing_provider"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn := oidcProviderArn("https://" + genRandString(16) + ".example.com")
|
||||
_, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityOIDCProviderDelete(arn))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagOpenIDConnectProvider_success(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage", "owner": "alice"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"env", "owner"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected UntagOpenIDConnectProvider response request id")
|
||||
}
|
||||
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{"team": "storage"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagOpenIDConnectProvider_removal_is_idempotent covers the two ways a
|
||||
// request can name a key that removes nothing: a key the provider never
|
||||
// carried, and the same key twice in one request. Neither is an error —
|
||||
// unlike TagOpenIDConnectProvider, which rejects a repeated key outright.
|
||||
func IAMUntagOpenIDConnectProvider_removal_is_idempotent(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_removal_is_idempotent"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"never-existed"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing a key the provider does not carry: %w", err)
|
||||
}
|
||||
if err := checkIAMOIDCProviderTags(client, arn, map[string]string{"env": "prod"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"env", "env"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing the same key twice: %w", err)
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagOpenIDConnectProvider_case_sensitive_key covers removal by a
|
||||
// differently-cased key: provider tag keys are compared exactly, so the
|
||||
// stored tag survives and only its exact key removes it.
|
||||
func IAMUntagOpenIDConnectProvider_case_sensitive_key(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_case_sensitive_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"EnV"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIAMOIDCProviderTags(client, arn, map[string]string{"env": "prod"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"env"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagOpenIDConnectProvider_removes_only_named_keys covers a partial
|
||||
// removal leaving the rest of the set intact, including a key whose name
|
||||
// only prefixes one of the supplied keys — matching is exact, not by prefix.
|
||||
func IAMUntagOpenIDConnectProvider_removes_only_named_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagOpenIDConnectProvider_removes_only_named_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
arn, err := createTestOIDCProvider(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagOIDCProvider(client, &iam.TagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "environment": "prod", "team": "storage"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := untagOIDCProvider(client, &iam.UntagOpenIDConnectProviderInput{
|
||||
OpenIDConnectProviderArn: &arn,
|
||||
TagKeys: []string{"env"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMOIDCProviderTags(client, arn, map[string]string{"environment": "prod", "team": "storage"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteOIDCProvider(client, arn)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func untagOIDCProvider(client *iam.Client, input *iam.UntagOpenIDConnectProviderInput) (*iam.UntagOpenIDConnectProviderOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.UntagOpenIDConnectProvider(ctx, input)
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMUntagRole_missing_role_name(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_missing_role_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"TagKeys.member.1": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_invalid_role_name(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_invalid_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: aws.String("invalid role name"),
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("roleName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_role_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_role_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 65)),
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_missing_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_missing_tag_keys"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagRole"},
|
||||
"Version": {"2010-05-08"},
|
||||
"RoleName": {"validrolename"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tagKeys"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_invalid_tag_key(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_invalid_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, tagKey := range []string{"", strings.Repeat("k", 129), "invalid*key"} {
|
||||
_, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
TagKeys: []string{tagKey},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrInvalidTagKeys)); checkErr != nil {
|
||||
return fmt.Errorf("tag key %q: %w", tagKey, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_too_many_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_too_many_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tagKeys := make([]string, 0, maxIAMTagMembersPerRequest+1)
|
||||
for i := range maxIAMTagMembersPerRequest + 1 {
|
||||
tagKeys = append(tagKeys, fmt.Sprintf("key%d", i+1))
|
||||
}
|
||||
|
||||
_, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: aws.String("validrolename"),
|
||||
TagKeys: tagKeys,
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTagKeys))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_non_existing_role(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_non_existing_role"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName := "non-existing-" + genRandString(16)
|
||||
_, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityRole(roleName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagRole_success(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage", "owner": "alice"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"env", "owner"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected UntagRole response request id")
|
||||
}
|
||||
|
||||
return checkIAMRoleTags(client, roleName, map[string]string{"team": "storage"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagRole_removal_is_idempotent covers the two ways a request can name
|
||||
// a key that removes nothing: a key the role never carried, and the same
|
||||
// key twice in one request. Neither is an error — unlike TagRole, which
|
||||
// rejects a repeated key outright.
|
||||
func IAMUntagRole_removal_is_idempotent(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_removal_is_idempotent"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"never-existed"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing a key the role does not carry: %w", err)
|
||||
}
|
||||
if err := checkIAMRoleTags(client, roleName, map[string]string{"env": "prod"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"env", "env"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing the same key twice: %w", err)
|
||||
}
|
||||
return checkIAMRoleTags(client, roleName, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagRole_case_insensitive_key covers removal by a differently-cased
|
||||
// key: IAM compares tag keys case-insensitively, so the tag is removed even
|
||||
// though the supplied key does not match the stored casing.
|
||||
func IAMUntagRole_case_insensitive_key(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_case_insensitive_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"EnV"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMRoleTags(client, roleName, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagRole_removes_only_named_keys covers a partial removal leaving the
|
||||
// rest of the set intact, including a key whose name only prefixes one of
|
||||
// the supplied keys — matching is exact, not by prefix.
|
||||
func IAMUntagRole_removes_only_named_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagRole_removes_only_named_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
roleName, err := createTaggableIAMRole(client, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMRole(client, &iam.TagRoleInput{
|
||||
RoleName: &roleName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "environment": "prod", "team": "storage"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := untagIAMRole(client, &iam.UntagRoleInput{
|
||||
RoleName: &roleName,
|
||||
TagKeys: []string{"env"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMRoleTags(client, roleName, map[string]string{"environment": "prod", "team": "storage"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMRole(client, roleName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func untagIAMRole(client *iam.Client, input *iam.UntagRoleInput) (*iam.UntagRoleOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.UntagRole(ctx, input)
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
)
|
||||
|
||||
func IAMUntagUser_missing_user_name(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_missing_user_name"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"TagKeys.member.1": {"env"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_invalid_user_name(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_invalid_user_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: aws.String("invalid user name"),
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.InvalidUserName("userName"))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_user_name_too_long(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_user_name_too_long"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: aws.String(strings.Repeat("a", 129)),
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("userName", 128))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_missing_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_missing_tag_keys"
|
||||
body := []byte(url.Values{
|
||||
"Action": {"UntagUser"},
|
||||
"Version": {"2010-05-08"},
|
||||
"UserName": {"validusername"},
|
||||
}.Encode())
|
||||
return authHandler(s, &authConfig{
|
||||
testName: testName,
|
||||
method: http.MethodPost,
|
||||
service: "iam",
|
||||
region: iamAuthRegion,
|
||||
body: body,
|
||||
date: time.Now().UTC(),
|
||||
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}, func(req *http.Request) error {
|
||||
return checkIAMAuthRequest(s, req, iamerr.MissingValue("tagKeys"))
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagUser_invalid_tag_key covers every malformed TagKeys member the
|
||||
// same way IAM does: whichever constraint fails — empty, over-long, or
|
||||
// outside the allowed charset — the response is one generic error naming
|
||||
// the whole constraint set, not the individual constraint that tripped.
|
||||
func IAMUntagUser_invalid_tag_key(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_invalid_tag_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
for _, tagKey := range []string{"", strings.Repeat("k", 129), "invalid*key"} {
|
||||
_, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
TagKeys: []string{tagKey},
|
||||
})
|
||||
if checkErr := checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrInvalidTagKeys)); checkErr != nil {
|
||||
return fmt.Errorf("tag key %q: %w", tagKey, checkErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_too_many_tag_keys(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_too_many_tag_keys"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
tagKeys := make([]string, 0, maxIAMTagMembersPerRequest+1)
|
||||
for i := range maxIAMTagMembersPerRequest + 1 {
|
||||
tagKeys = append(tagKeys, fmt.Sprintf("key%d", i+1))
|
||||
}
|
||||
|
||||
_, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: aws.String("validusername"),
|
||||
TagKeys: tagKeys,
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.GetAPIError(iamerr.ErrTooManyTagKeys))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_non_existing_user(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_non_existing_user"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := "non-existing-" + genRandString(16)
|
||||
_, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: &userName,
|
||||
TagKeys: []string{"env"},
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.NoSuchEntityUser(userName))
|
||||
})
|
||||
}
|
||||
|
||||
func IAMUntagUser_success(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_success"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod", "team": "storage", "owner": "alice"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: &userName,
|
||||
TagKeys: []string{"env", "owner"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata); !ok || requestID == "" {
|
||||
return fmt.Errorf("expected UntagUser response request id")
|
||||
}
|
||||
|
||||
return checkIAMUserTags(client, userName, map[string]string{"team": "storage"})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagUser_removal_is_idempotent covers the two ways a request can name
|
||||
// a key that removes nothing: a key the user never carried, and the same
|
||||
// key twice in one request. Neither is an error — unlike TagUser, which
|
||||
// rejects a repeated key outright.
|
||||
func IAMUntagUser_removal_is_idempotent(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_removal_is_idempotent"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: &userName,
|
||||
TagKeys: []string{"never-existed"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing a key the user does not carry: %w", err)
|
||||
}
|
||||
if err := checkIAMUserTags(client, userName, map[string]string{"env": "prod"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: &userName,
|
||||
TagKeys: []string{"env", "env"},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("removing the same key twice: %w", err)
|
||||
}
|
||||
return checkIAMUserTags(client, userName, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
// IAMUntagUser_case_insensitive_key covers removal by a differently-cased
|
||||
// key: IAM compares tag keys case-insensitively, so the tag is removed even
|
||||
// though the supplied key does not match the stored casing.
|
||||
func IAMUntagUser_case_insensitive_key(s *S3Conf) error {
|
||||
testName := "IAMUntagUser_case_insensitive_key"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
userName := newIAMUserName()
|
||||
if _, err := createIAMUser(client, &iam.CreateUserInput{UserName: &userName}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr := func() error {
|
||||
if _, err := tagIAMUser(client, &iam.TagUserInput{
|
||||
UserName: &userName,
|
||||
Tags: iamTagList(map[string]string{"env": "prod"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := untagIAMUser(client, &iam.UntagUserInput{
|
||||
UserName: &userName,
|
||||
TagKeys: []string{"EnV"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkIAMUserTags(client, userName, map[string]string{})
|
||||
}()
|
||||
|
||||
deleteErr := deleteIAMUser(client, userName)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
}
|
||||
return deleteErr
|
||||
})
|
||||
}
|
||||
|
||||
func untagIAMUser(client *iam.Client, input *iam.UntagUserInput) (*iam.UntagUserOutput, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
return client.UntagUser(ctx, input)
|
||||
}
|
||||
@@ -88,10 +88,10 @@ func IAMUpdateAssumeRolePolicy_long_role_name(s *S3Conf) error {
|
||||
testName := "IAMUpdateAssumeRolePolicy_long_role_name"
|
||||
return iamActionHandler(s, testName, func(client *iam.Client) error {
|
||||
_, err := updateIAMAssumeRolePolicy(client, &iam.UpdateAssumeRolePolicyInput{
|
||||
RoleName: aws.String(strings.Repeat("a", 129)),
|
||||
RoleName: aws.String(strings.Repeat("a", 65)),
|
||||
PolicyDocument: aws.String(validTrustPolicyDocument),
|
||||
})
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 128))
|
||||
return checkIAMApiErr(err, iamerr.UserNameTooLong("roleName", 64))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user