mirror of
https://github.com/versity/versitygw.git
synced 2026-09-25 09:24:22 +00:00
feat: add IAM user inline policy CRUD
Add support for AWS-compatible inline identity-based policies on IAM users, implementing the `PutUserPolicy`, `GetUserPolicy`, `DeleteUserPolicy`, and `ListUserPolicies` actions on both the internal and Vault storage backends. - iamapi/policy is a new package that parses and validates policy documents against IAM's parameter-level constraints (max length, allowed charset) and policy grammar (Version, Effect, mutually exclusive Action/NotAction and Resource/NotResource, vendor-prefixed actions, ARN-shaped resources, no Principal/NotPrincipal, unique Sids). - `PutUserPolicy` creates or replaces a named inline policy on a user, enforcing a 2048-byte aggregate quota across all of a user's inline policies (MaxInlinePolicyBytesPerUser), matching the AWS IAM quota. - `GetUserPolicy` returns a policy's document RFC 3986 percent-encoded, matching how real IAM encodes the PolicyDocument response element. - `DeleteUserPolicy` removes a named inline policy from a user. - `ListUserPolicies` returns a paginated, sorted list of a user's inline policy names, honoring Marker/MaxItems like the other IAM list APIs. - `DeleteUser` is now rejected with a DeleteConflict error if the user still has inline policies attached, mirroring the existing access-key delete-conflict behavior.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
// 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 iamutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncodePolicyDocument RFC 3986 percent-encodes a policy document string
|
||||
// the way real IAM encodes the PolicyDocument element of GetUserPolicy (and
|
||||
// will for GetRolePolicy) responses: every character outside the unreserved
|
||||
// set is percent-encoded, with the space character encoded as %20 rather
|
||||
// than the "+" that url.QueryEscape alone would produce.
|
||||
func EncodePolicyDocument(s string) string {
|
||||
return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
@@ -43,9 +44,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
userNamePattern = regexp.MustCompile(`^[A-Za-z0-9+=,.@_-]+$`)
|
||||
tagKeyPattern = regexp.MustCompile(`^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$`)
|
||||
tagValPattern = regexp.MustCompile(`^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$`)
|
||||
namePattern = regexp.MustCompile(`^[A-Za-z0-9+=,.@_-]+$`)
|
||||
tagKeyPattern = regexp.MustCompile(`^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$`)
|
||||
tagValPattern = regexp.MustCompile(`^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$`)
|
||||
)
|
||||
|
||||
// RequestParam looks up key first in URL query args, then in the POST body.
|
||||
@@ -63,6 +64,42 @@ func RequestParam(ctx fiber.Ctx, key string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetUserName resolves the UserName request parameter and validates it
|
||||
// against maxLen, returning missingErr if the parameter is absent or empty.
|
||||
// operation is included in the debug log on failure (e.g. "DeleteUser").
|
||||
// missingErr lets callers match the exact AWS error their operation is
|
||||
// verified against (e.g. iamerr.MissingValue vs iamerr.MissingParameter).
|
||||
func GetUserName(ctx fiber.Ctx, operation string, maxLen int, missingErr error) (string, error) {
|
||||
userName, ok := RequestParam(ctx, "UserName")
|
||||
if !ok || userName == "" {
|
||||
debuglogger.Logf("missing required %s parameter: UserName", operation)
|
||||
return "", missingErr
|
||||
}
|
||||
if err := ValidateName("userName", userName, maxLen); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userName, nil
|
||||
}
|
||||
|
||||
// ParseMaxItems reads the MaxItems request parameter, defaulting to
|
||||
// DefaultMaxItems when absent. operation is included in the debug log on
|
||||
// parse failure (e.g. "ListUsers", "ListAccessKeys").
|
||||
func ParseMaxItems(ctx fiber.Ctx, operation string) (int32, error) {
|
||||
rawMaxItems, ok := RequestParam(ctx, "MaxItems")
|
||||
if !ok || rawMaxItems == "" {
|
||||
return int32(DefaultMaxItems), nil
|
||||
}
|
||||
|
||||
parsed, err := strconv.ParseInt(rawMaxItems, 10, 32)
|
||||
if err != nil || parsed < 1 || parsed > MaxListItems {
|
||||
debuglogger.Logf("invalid %s MaxItems value %q: parse_error=%v", operation, rawMaxItems, err)
|
||||
return 0, iamerr.InvalidMaxItems(rawMaxItems)
|
||||
}
|
||||
|
||||
return int32(parsed), nil
|
||||
}
|
||||
|
||||
// ParseTags reads IAM tag members from the request (up to 50), validates each, and returns the list.
|
||||
func ParseTags(ctx fiber.Ctx) ([]types.Tag, error) {
|
||||
var tags []types.Tag
|
||||
@@ -106,14 +143,16 @@ func ParseTags(ctx fiber.Ctx) ([]types.Tag, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// ValidateUserName checks that userName is non-empty, matches the allowed character set, and fits within maxLength.
|
||||
func ValidateUserName(field, userName string, maxLength int) error {
|
||||
if len(userName) > maxLength {
|
||||
debuglogger.Logf("IAM user name exceeds maximum length: field=%s length=%d max=%d", field, len(userName), maxLength)
|
||||
// ValidateName checks that name (an IAM identity or policy name, e.g.
|
||||
// userName or policyName) is non-empty, matches the allowed character set,
|
||||
// and fits within maxLength.
|
||||
func ValidateName(field, name string, maxLength int) error {
|
||||
if len(name) > maxLength {
|
||||
debuglogger.Logf("IAM name exceeds maximum length: field=%s length=%d max=%d", field, len(name), maxLength)
|
||||
return iamerr.UserNameTooLong(field, maxLength)
|
||||
}
|
||||
if userName == "" || !userNamePattern.MatchString(userName) {
|
||||
debuglogger.Logf("invalid IAM user name: field=%s value=%q", field, userName)
|
||||
if name == "" || !namePattern.MatchString(name) {
|
||||
debuglogger.Logf("invalid IAM name: field=%s value=%q", field, name)
|
||||
return iamerr.InvalidUserName(field)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user