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
+74 -6
View File
@@ -349,7 +349,7 @@ func (s *InternalStore) UpdateUser(_ context.Context, input UpdateUserInput) (*t
func (s *InternalStore) TagUser(_ context.Context, userName string, tags []types.Tag) error {
return s.updateUserTags(userName, func(user *types.User) error {
merged, err := mergeTags(user.Tags, tags)
merged, err := mergeTags(user.Tags, tags, iamutil.TagKeysFolded)
if err != nil {
return err
}
@@ -360,7 +360,7 @@ func (s *InternalStore) TagUser(_ context.Context, userName string, tags []types
func (s *InternalStore) UntagUser(_ context.Context, userName string, tagKeys []string) error {
return s.updateUserTags(userName, func(user *types.User) error {
user.Tags = removeTags(user.Tags, tagKeys)
user.Tags = removeTags(user.Tags, tagKeys, iamutil.TagKeysFolded)
return nil
})
}
@@ -405,7 +405,7 @@ func (s *InternalStore) ListUserTags(_ context.Context, input ListUserTagsInput)
return nil, iamerr.NoSuchEntityUser(input.UserName)
}
return paginateTags(user.Tags, input.Marker, input.MaxItems), nil
return paginateTags(user.Tags, input.Marker, input.MaxItems, iamutil.TagKeysFolded), nil
}
func (s *InternalStore) CreateAccessKey(_ context.Context, input CreateAccessKeyInput) (*types.AccessKey, error) {
@@ -959,7 +959,7 @@ func (s *InternalStore) UpdateAssumeRolePolicy(_ context.Context, input UpdateAs
func (s *InternalStore) TagRole(_ context.Context, roleName string, tags []types.Tag) error {
return s.updateRoleTags(roleName, func(role *types.Role) error {
merged, err := mergeTags(role.Tags, tags)
merged, err := mergeTags(role.Tags, tags, iamutil.TagKeysFolded)
if err != nil {
return err
}
@@ -970,7 +970,7 @@ func (s *InternalStore) TagRole(_ context.Context, roleName string, tags []types
func (s *InternalStore) UntagRole(_ context.Context, roleName string, tagKeys []string) error {
return s.updateRoleTags(roleName, func(role *types.Role) error {
role.Tags = removeTags(role.Tags, tagKeys)
role.Tags = removeTags(role.Tags, tagKeys, iamutil.TagKeysFolded)
return nil
})
}
@@ -1015,7 +1015,7 @@ func (s *InternalStore) ListRoleTags(_ context.Context, input ListRoleTagsInput)
return nil, iamerr.NoSuchEntityRole(input.RoleName)
}
return paginateTags(role.Tags, input.Marker, input.MaxItems), nil
return paginateTags(role.Tags, input.Marker, input.MaxItems, iamutil.TagKeysFolded), nil
}
func (s *InternalStore) PutRolePolicy(_ context.Context, input PutRolePolicyInput) error {
@@ -1357,6 +1357,74 @@ func (s *InternalStore) UpdateOIDCProviderThumbprint(_ context.Context, arn stri
return unwrapAPIError(err)
}
func (s *InternalStore) TagOIDCProvider(_ context.Context, arn string, tags []types.Tag) error {
return s.updateOIDCProviderTags(arn, func(provider *types.OIDCProvider) error {
merged, err := mergeTags(provider.Tags, tags, iamutil.TagKeysExact)
if err != nil {
return err
}
provider.Tags = merged
return nil
})
}
func (s *InternalStore) UntagOIDCProvider(_ context.Context, arn string, tagKeys []string) error {
return s.updateOIDCProviderTags(arn, func(provider *types.OIDCProvider) error {
provider.Tags = removeTags(provider.Tags, tagKeys, iamutil.TagKeysExact)
return nil
})
}
// updateOIDCProviderTags applies mutate to arn's stored record and writes
// it back under the store lock.
func (s *InternalStore) updateOIDCProviderTags(arn string, mutate func(*types.OIDCProvider) error) error {
s.Lock()
defer s.Unlock()
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
conf, err := s.engine.ParseIAM(data)
if err != nil {
return nil, err
}
url, err := iamutil.ParseOIDCProviderArn(arn)
if err != nil {
return nil, err
}
provider, ok := conf.OIDCProviders[url]
if !ok {
return nil, iamerr.NoSuchEntityOIDCProviderDelete(arn)
}
if err := mutate(&provider); err != nil {
return nil, err
}
conf.OIDCProviders[url] = provider
return json.Marshal(conf)
})
return unwrapAPIError(err)
}
func (s *InternalStore) ListOIDCProviderTags(_ context.Context, input ListOIDCProviderTagsInput) (*ListTagsOutput, error) {
s.RLock()
defer s.RUnlock()
url, err := iamutil.ParseOIDCProviderArn(input.Arn)
if err != nil {
return nil, err
}
conf, err := s.engine.GetIAM()
if err != nil {
return nil, err
}
provider, ok := conf.OIDCProviders[url]
if !ok {
return nil, iamerr.NoSuchEntityOIDCProviderDelete(input.Arn)
}
return paginateTags(provider.Tags, input.Marker, input.MaxItems, iamutil.TagKeysExact), nil
}
func (s *InternalStore) CreateSession(_ context.Context, session types.Session) (*types.Session, error) {
s.Lock()
defer s.Unlock()