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:
niksis02
2026-07-10 03:17:23 +04:00
parent 001e7d88e4
commit 89b64910f3
20 changed files with 2645 additions and 78 deletions
+31 -7
View File
@@ -54,12 +54,12 @@ const (
ErrInvalidClientTokenID
ErrInvalidContentLength
ErrThrottling
ErrMissingUserNameValue
ErrTooManyTags
ErrInvalidPathPrefix
ErrDuplicateTagKeys
ErrInvalidAccessKeyIDChars
ErrDeleteConflict
ErrDeleteConflictPolicies
)
type APIError interface {
@@ -207,12 +207,6 @@ var errorCodeResponse = map[ErrorCode]Error{
Message: "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.",
HTTPStatusCode: http.StatusForbidden,
},
ErrMissingUserNameValue: {
Type: TypeSender,
Code: "ValidationError",
Message: "1 validation error detected: Value at 'userName' failed to satisfy constraint: Member must not be null",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPathPrefix: {
Type: TypeSender,
Code: "ValidationError",
@@ -243,6 +237,12 @@ var errorCodeResponse = map[ErrorCode]Error{
Message: "Cannot delete entity, must delete access keys first.",
HTTPStatusCode: http.StatusConflict,
},
ErrDeleteConflictPolicies: {
Type: TypeSender,
Code: "DeleteConflict",
Message: "Cannot delete entity, must delete policies first.",
HTTPStatusCode: http.StatusConflict,
},
}
func GetAPIError(code ErrorCode) Error {
@@ -405,6 +405,30 @@ func InvalidTagValue(index int) Error {
return ValidationError(fmt.Sprintf("1 validation error detected: Value at 'tags.%d.member.value' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*", index))
}
func MissingValue(field string) Error {
return ValidationError(fmt.Sprintf("1 validation error detected: Value at '%s' failed to satisfy constraint: Member must not be null", field))
}
func ValueTooLong(field string, maxLength int) Error {
return ValidationError(fmt.Sprintf("1 validation error detected: Value at '%s' failed to satisfy constraint: Member must have length less than or equal to %d", field, maxLength))
}
func InvalidCharset(field string) Error {
return ValidationError(fmt.Sprintf("The specified value for %s is invalid. It must contain only printable ASCII characters.", field))
}
func MalformedPolicyDocument(message string) Error {
return newSenderError("MalformedPolicyDocument", message, http.StatusBadRequest)
}
func NoSuchEntityUserPolicy(userName, policyName string) Error {
return newSenderError("NoSuchEntity", fmt.Sprintf("The user policy with name %s cannot be found.", policyName), http.StatusNotFound)
}
func InlinePolicyQuotaExceeded(entityKind, entityName string, maxBytes int) Error {
return newSenderError("LimitExceeded", fmt.Sprintf("Maximum policy size of %d bytes exceeded for %s %s", maxBytes, entityKind, entityName), http.StatusConflict)
}
func newSenderError(code, message string, statusCode int) Error {
return Error{
Type: TypeSender,