feat: add IAM role tagging actions

Adds `TagRole`, `UntagRole` and `ListRoleTags` to the standalone IAM service, backed by both the internal and Vault storers, with the same semantics the user tagging actions already have: tag keys are matched case-insensitively but stored case-preserving, TagRole merges into the role's existing tags and rejects duplicate keys, UntagRole removal is idempotent, and ListRoleTags is sorted by key and paginated. The per-request member count and the per-role tag total are enforced as separate quotas, so replacing a tag on a role already at the 50-tag cap still succeeds. Role tags and the tags of a user sharing the same name are independent sets.

All three actions are authorized against the target role's ARN, so `aws:ResourceTag/<key>` reads the role's own tags, and TagRole and UntagRole populate `aws:RequestTag/<key>` and `aws:TagKeys` respectively, so a tag-scoped policy Condition governs which tags a caller may set or remove.

The tag storage helpers are now shared between users and roles: `MaxTagsPerUser` becomes `MaxTagsPerResource`, `ListUserTagsOutput` becomes `ListTagsOutput`, and `paginateTags` takes the marker and page size directly instead of a user-specific input struct.

The WebGUI gains a Tags section in the IAM role manage view, reusing the tag editor the user view already uses, which applies a whole edited tag set as a single UntagRole and TagRole pair.

Also corrects the `roleName` length bound across every role action: it was validated against the 128-character user-lookup limit, where IAM caps role names at 64.
This commit is contained in:
niksis02
2026-08-28 00:49:20 +04:00
parent 26a54b33e9
commit 1bbcd64195
22 changed files with 1947 additions and 82 deletions
+88 -8
View File
@@ -624,7 +624,7 @@ func (c IAMApiController) ListUserPolicies(ctx fiber.Ctx) (*Response, error) {
}
func (c IAMApiController) CreateRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "CreateRole", iamutil.MaxUserNameLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "CreateRole", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
@@ -708,7 +708,7 @@ func (c IAMApiController) CreateRole(ctx fiber.Ctx) (*Response, error) {
}
func (c IAMApiController) GetRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "GetRole", iamutil.MaxUserLookupLen, iamerr.MissingParameter("RoleName"))
roleName, err := iamutil.GetRoleName(ctx, "GetRole", iamutil.MaxRoleNameLen, iamerr.MissingParameter("RoleName"))
if err != nil {
return nil, err
}
@@ -767,7 +767,7 @@ func (c IAMApiController) ListRoles(ctx fiber.Ctx) (*Response, error) {
}
func (c IAMApiController) DeleteRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "DeleteRole", iamutil.MaxUserLookupLen, iamerr.MissingParameter("RoleName"))
roleName, err := iamutil.GetRoleName(ctx, "DeleteRole", iamutil.MaxRoleNameLen, iamerr.MissingParameter("RoleName"))
if err != nil {
return nil, err
}
@@ -790,7 +790,7 @@ func (c IAMApiController) UpdateAssumeRolePolicy(ctx fiber.Ctx) (*Response, erro
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "UpdateAssumeRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "UpdateAssumeRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
@@ -819,6 +819,86 @@ func (c IAMApiController) UpdateAssumeRolePolicy(ctx fiber.Ctx) (*Response, erro
return &Response{Data: &types.UpdateAssumeRolePolicyResponse{}}, nil
}
// TagRole adds or replaces tags on an existing role.
func (c IAMApiController) TagRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "TagRole", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
tags, err := iamutil.ParseTags(ctx)
if err != nil {
return nil, err
}
if len(tags) == 0 {
debuglogger.Logf("missing required TagRole parameter: Tags")
return nil, iamerr.MissingValue("tags")
}
if err := c.store.TagRole(ctx.Context(), roleName, tags); err != nil {
debuglogger.Logf("failed to tag IAM role %q: %v", roleName, err)
return nil, err
}
return &Response{Data: &types.TagRoleResponse{}}, nil
}
// UntagRole removes the named tags from an existing role. Removal is
// idempotent: a key naming no current tag is not an error.
func (c IAMApiController) UntagRole(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "UntagRole", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
tagKeys, err := iamutil.ParseTagKeys(ctx)
if err != nil {
return nil, err
}
if len(tagKeys) == 0 {
debuglogger.Logf("missing required UntagRole parameter: TagKeys")
return nil, iamerr.MissingValue("tagKeys")
}
if err := c.store.UntagRole(ctx.Context(), roleName, tagKeys); err != nil {
debuglogger.Logf("failed to untag IAM role %q: %v", roleName, err)
return nil, err
}
return &Response{Data: &types.UntagRoleResponse{}}, nil
}
func (c IAMApiController) ListRoleTags(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "ListRoleTags", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
maxItems, err := iamutil.ParseMaxItems(ctx, "ListRoleTags")
if err != nil {
return nil, err
}
marker, _ := iamutil.RequestParam(ctx, "Marker")
out, err := c.store.ListRoleTags(ctx.Context(), storage.ListRoleTagsInput{
RoleName: roleName,
Marker: marker,
MaxItems: maxItems,
})
if err != nil {
debuglogger.Logf("failed to list IAM role %q tags: %v", roleName, err)
return nil, err
}
return &Response{Data: &types.ListRoleTagsResponse{
Result: types.ListRoleTagsResult{
Tags: types.Tags{Members: out.Tags},
IsTruncated: out.IsTruncated,
Marker: out.Marker,
},
}}, nil
}
func (c IAMApiController) PutRolePolicy(ctx fiber.Ctx) (*Response, error) {
policyDocument, ok := iamutil.RequestParam(ctx, "PolicyDocument")
if !ok {
@@ -838,7 +918,7 @@ func (c IAMApiController) PutRolePolicy(ctx fiber.Ctx) (*Response, error) {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "PutRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "PutRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
@@ -875,7 +955,7 @@ func (c IAMApiController) GetRolePolicy(ctx fiber.Ctx) (*Response, error) {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "GetRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "GetRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
@@ -905,7 +985,7 @@ func (c IAMApiController) DeleteRolePolicy(ctx fiber.Ctx) (*Response, error) {
return nil, err
}
roleName, err := iamutil.GetRoleName(ctx, "DeleteRolePolicy", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "DeleteRolePolicy", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}
@@ -919,7 +999,7 @@ func (c IAMApiController) DeleteRolePolicy(ctx fiber.Ctx) (*Response, error) {
}
func (c IAMApiController) ListRolePolicies(ctx fiber.Ctx) (*Response, error) {
roleName, err := iamutil.GetRoleName(ctx, "ListRolePolicies", iamutil.MaxUserLookupLen, iamerr.MissingValue("roleName"))
roleName, err := iamutil.GetRoleName(ctx, "ListRolePolicies", iamutil.MaxRoleNameLen, iamerr.MissingValue("roleName"))
if err != nil {
return nil, err
}