feat: add IAM OIDC provider tagging actions

Adds `TagOpenIDConnectProvider`, `UntagOpenIDConnectProvider` and `ListOpenIDConnectProviderTags` to the standalone IAM service, backed by both the internal and Vault storers. They follow the user and role tagging actions in most respects — the tag action merges into the provider's existing tags and rejects a repeated key, untag removal is idempotent, and the tag listing is sorted by key and paginated, with the per-request member count and the per-provider tag total enforced as separate quotas so replacing a tag on a provider already at the 50-tag cap still succeeds — but differ in the one respect IAM itself draws: OIDC provider tag keys are compared exactly, not case-insensitively. On a provider `env` and `ENV` are two independent tags, both may be supplied in a single request, only a byte-identical repeat is a duplicate (reported without the "Tag keys are case insensitive" note the user and role actions carry), and untagging `env` leaves `ENV` in place.

That distinction is now carried by `iamutil.TagKeyCase`, which `ParseTags` uses for duplicate detection and which `mergeTags`, `removeTags` and the tag listing's marker lookup use for key matching. `CreateOpenIDConnectProvider` moves onto the exact comparison too, so a provider created with case-differing tag keys keeps both.

All three actions are authorized against the target provider's ARN, so `aws:ResourceTag/<key>` reads the provider's own tags, and the tag and untag actions populate `aws:RequestTag/<key>` and `aws:TagKeys` respectively, so a tag-scoped policy Condition governs which tags a caller may set or remove. All three report a missing provider with the wording `DeleteOpenIDConnectProvider` uses rather than the one `GetOpenIDConnectProvider` uses, which is why the Vault provider read now takes the not-found error its calling action reports.

The WebGUI gains a Tags section in the OIDC provider manage view, replacing the read-only tag row, and the shared tag editor gains a case-sensitive mode that changes its duplicate-key check, its diffing of an edited set into an untag and tag pair, and the wording of its guidance.
This commit is contained in:
niksis02
2026-08-28 00:49:21 +04:00
parent 1bbcd64195
commit 4901afe27b
19 changed files with 2086 additions and 94 deletions
+76
View File
@@ -1551,6 +1551,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)
@@ -1756,6 +1797,9 @@ func TestIAM(ts *TestState) {
TestIAMAddClientIDToOpenIDConnectProvider(ts)
TestIAMRemoveClientIDFromOpenIDConnectProvider(ts)
TestIAMUpdateOpenIDConnectProviderThumbprint(ts)
TestIAMTagOpenIDConnectProvider(ts)
TestIAMUntagOpenIDConnectProvider(ts)
TestIAMListOpenIDConnectProviderTags(ts)
TestIAMAssumeRoleWithWebIdentity(ts)
TestIAMGetCallerIdentity(ts)
TestIAMAccessControl(ts)
@@ -2487,6 +2531,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,
+36 -2
View File
@@ -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()),
@@ -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)
}
+454
View File
@@ -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,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)
}